Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/prompt_toolkit/__init__.py +54 -0
- lib/python3.12/site-packages/prompt_toolkit/auto_suggest.py +177 -0
- lib/python3.12/site-packages/prompt_toolkit/buffer.py +2029 -0
- lib/python3.12/site-packages/prompt_toolkit/cache.py +127 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/__init__.py +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/completers/__init__.py +5 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/completers/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/completers/__pycache__/system.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/completers/system.py +64 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__init__.py +80 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/compiler.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/completion.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/lexer.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/regex_parser.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/validation.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/compiler.py +579 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/completion.py +100 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/lexer.py +94 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/regex_parser.py +279 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/validation.py +60 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/__init__.py +8 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/__pycache__/server.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/server.py +178 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__init__.py +7 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/log.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/protocol.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/server.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/log.py +13 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/protocol.py +209 -0
- lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/server.py +428 -0
- lib/python3.12/site-packages/prompt_toolkit/cursor_shapes.py +117 -0
- lib/python3.12/site-packages/prompt_toolkit/data_structures.py +18 -0
- lib/python3.12/site-packages/prompt_toolkit/document.py +1182 -0
- lib/python3.12/site-packages/prompt_toolkit/enums.py +19 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/__init__.py +71 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/app.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/base.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/cli.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/app.py +419 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/base.py +260 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/cli.py +65 -0
- lib/python3.12/site-packages/prompt_toolkit/filters/utils.py +41 -0
- lib/python3.12/site-packages/prompt_toolkit/history.py +306 -0
- lib/python3.12/site-packages/prompt_toolkit/key_binding/__init__.py +22 -0
lib/python3.12/site-packages/prompt_toolkit/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
prompt_toolkit
|
| 3 |
+
==============
|
| 4 |
+
|
| 5 |
+
Author: Jonathan Slenders
|
| 6 |
+
|
| 7 |
+
Description: prompt_toolkit is a Library for building powerful interactive
|
| 8 |
+
command lines in Python. It can be a replacement for GNU
|
| 9 |
+
Readline, but it can be much more than that.
|
| 10 |
+
|
| 11 |
+
See the examples directory to learn about the usage.
|
| 12 |
+
|
| 13 |
+
Probably, to get started, you might also want to have a look at
|
| 14 |
+
`prompt_toolkit.shortcuts.prompt`.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import re
|
| 20 |
+
from importlib import metadata
|
| 21 |
+
|
| 22 |
+
# note: this is a bit more lax than the actual pep 440 to allow for a/b/rc/dev without a number
|
| 23 |
+
pep440 = re.compile(
|
| 24 |
+
r"^([1-9]\d*!)?(0|[1-9]\d*)(\.(0|[1-9]\d*))*((a|b|rc)(0|[1-9]\d*)?)?(\.post(0|[1-9]\d*))?(\.dev(0|[1-9]\d*)?)?$",
|
| 25 |
+
re.UNICODE,
|
| 26 |
+
)
|
| 27 |
+
from .application import Application
|
| 28 |
+
from .formatted_text import ANSI, HTML
|
| 29 |
+
from .shortcuts import PromptSession, choice, print_formatted_text, prompt
|
| 30 |
+
|
| 31 |
+
# Don't forget to update in `docs/conf.py`!
|
| 32 |
+
__version__ = metadata.version("prompt_toolkit")
|
| 33 |
+
|
| 34 |
+
assert pep440.match(__version__)
|
| 35 |
+
|
| 36 |
+
# Version tuple.
|
| 37 |
+
VERSION = tuple(int(v.rstrip("abrc")) for v in __version__.split(".")[:3])
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
__all__ = [
|
| 41 |
+
# Application.
|
| 42 |
+
"Application",
|
| 43 |
+
# Shortcuts.
|
| 44 |
+
"prompt",
|
| 45 |
+
"choice",
|
| 46 |
+
"PromptSession",
|
| 47 |
+
"print_formatted_text",
|
| 48 |
+
# Formatted text.
|
| 49 |
+
"HTML",
|
| 50 |
+
"ANSI",
|
| 51 |
+
# Version info.
|
| 52 |
+
"__version__",
|
| 53 |
+
"VERSION",
|
| 54 |
+
]
|
lib/python3.12/site-packages/prompt_toolkit/auto_suggest.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
`Fish-style <http://fishshell.com/>`_ like auto-suggestion.
|
| 3 |
+
|
| 4 |
+
While a user types input in a certain buffer, suggestions are generated
|
| 5 |
+
(asynchronously.) Usually, they are displayed after the input. When the cursor
|
| 6 |
+
presses the right arrow and the cursor is at the end of the input, the
|
| 7 |
+
suggestion will be inserted.
|
| 8 |
+
|
| 9 |
+
If you want the auto suggestions to be asynchronous (in a background thread),
|
| 10 |
+
because they take too much time, and could potentially block the event loop,
|
| 11 |
+
then wrap the :class:`.AutoSuggest` instance into a
|
| 12 |
+
:class:`.ThreadedAutoSuggest`.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from abc import ABCMeta, abstractmethod
|
| 18 |
+
from typing import TYPE_CHECKING, Callable
|
| 19 |
+
|
| 20 |
+
from prompt_toolkit.eventloop import run_in_executor_with_context
|
| 21 |
+
|
| 22 |
+
from .document import Document
|
| 23 |
+
from .filters import Filter, to_filter
|
| 24 |
+
|
| 25 |
+
if TYPE_CHECKING:
|
| 26 |
+
from .buffer import Buffer
|
| 27 |
+
|
| 28 |
+
__all__ = [
|
| 29 |
+
"Suggestion",
|
| 30 |
+
"AutoSuggest",
|
| 31 |
+
"ThreadedAutoSuggest",
|
| 32 |
+
"DummyAutoSuggest",
|
| 33 |
+
"AutoSuggestFromHistory",
|
| 34 |
+
"ConditionalAutoSuggest",
|
| 35 |
+
"DynamicAutoSuggest",
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class Suggestion:
|
| 40 |
+
"""
|
| 41 |
+
Suggestion returned by an auto-suggest algorithm.
|
| 42 |
+
|
| 43 |
+
:param text: The suggestion text.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, text: str) -> None:
|
| 47 |
+
self.text = text
|
| 48 |
+
|
| 49 |
+
def __repr__(self) -> str:
|
| 50 |
+
return f"Suggestion({self.text})"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class AutoSuggest(metaclass=ABCMeta):
|
| 54 |
+
"""
|
| 55 |
+
Base class for auto suggestion implementations.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
@abstractmethod
|
| 59 |
+
def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None:
|
| 60 |
+
"""
|
| 61 |
+
Return `None` or a :class:`.Suggestion` instance.
|
| 62 |
+
|
| 63 |
+
We receive both :class:`~prompt_toolkit.buffer.Buffer` and
|
| 64 |
+
:class:`~prompt_toolkit.document.Document`. The reason is that auto
|
| 65 |
+
suggestions are retrieved asynchronously. (Like completions.) The
|
| 66 |
+
buffer text could be changed in the meantime, but ``document`` contains
|
| 67 |
+
the buffer document like it was at the start of the auto suggestion
|
| 68 |
+
call. So, from here, don't access ``buffer.text``, but use
|
| 69 |
+
``document.text`` instead.
|
| 70 |
+
|
| 71 |
+
:param buffer: The :class:`~prompt_toolkit.buffer.Buffer` instance.
|
| 72 |
+
:param document: The :class:`~prompt_toolkit.document.Document` instance.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
async def get_suggestion_async(
|
| 76 |
+
self, buff: Buffer, document: Document
|
| 77 |
+
) -> Suggestion | None:
|
| 78 |
+
"""
|
| 79 |
+
Return a :class:`.Future` which is set when the suggestions are ready.
|
| 80 |
+
This function can be overloaded in order to provide an asynchronous
|
| 81 |
+
implementation.
|
| 82 |
+
"""
|
| 83 |
+
return self.get_suggestion(buff, document)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class ThreadedAutoSuggest(AutoSuggest):
|
| 87 |
+
"""
|
| 88 |
+
Wrapper that runs auto suggestions in a thread.
|
| 89 |
+
(Use this to prevent the user interface from becoming unresponsive if the
|
| 90 |
+
generation of suggestions takes too much time.)
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
def __init__(self, auto_suggest: AutoSuggest) -> None:
|
| 94 |
+
self.auto_suggest = auto_suggest
|
| 95 |
+
|
| 96 |
+
def get_suggestion(self, buff: Buffer, document: Document) -> Suggestion | None:
|
| 97 |
+
return self.auto_suggest.get_suggestion(buff, document)
|
| 98 |
+
|
| 99 |
+
async def get_suggestion_async(
|
| 100 |
+
self, buff: Buffer, document: Document
|
| 101 |
+
) -> Suggestion | None:
|
| 102 |
+
"""
|
| 103 |
+
Run the `get_suggestion` function in a thread.
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
def run_get_suggestion_thread() -> Suggestion | None:
|
| 107 |
+
return self.get_suggestion(buff, document)
|
| 108 |
+
|
| 109 |
+
return await run_in_executor_with_context(run_get_suggestion_thread)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class DummyAutoSuggest(AutoSuggest):
|
| 113 |
+
"""
|
| 114 |
+
AutoSuggest class that doesn't return any suggestion.
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None:
|
| 118 |
+
return None # No suggestion
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class AutoSuggestFromHistory(AutoSuggest):
|
| 122 |
+
"""
|
| 123 |
+
Give suggestions based on the lines in the history.
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None:
|
| 127 |
+
history = buffer.history
|
| 128 |
+
|
| 129 |
+
# Consider only the last line for the suggestion.
|
| 130 |
+
text = document.text.rsplit("\n", 1)[-1]
|
| 131 |
+
|
| 132 |
+
# Only create a suggestion when this is not an empty line.
|
| 133 |
+
if text.strip():
|
| 134 |
+
# Find first matching line in history.
|
| 135 |
+
for string in reversed(list(history.get_strings())):
|
| 136 |
+
for line in reversed(string.splitlines()):
|
| 137 |
+
if line.startswith(text):
|
| 138 |
+
return Suggestion(line[len(text) :])
|
| 139 |
+
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class ConditionalAutoSuggest(AutoSuggest):
|
| 144 |
+
"""
|
| 145 |
+
Auto suggest that can be turned on and of according to a certain condition.
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
def __init__(self, auto_suggest: AutoSuggest, filter: bool | Filter) -> None:
|
| 149 |
+
self.auto_suggest = auto_suggest
|
| 150 |
+
self.filter = to_filter(filter)
|
| 151 |
+
|
| 152 |
+
def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None:
|
| 153 |
+
if self.filter():
|
| 154 |
+
return self.auto_suggest.get_suggestion(buffer, document)
|
| 155 |
+
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class DynamicAutoSuggest(AutoSuggest):
|
| 160 |
+
"""
|
| 161 |
+
Validator class that can dynamically returns any Validator.
|
| 162 |
+
|
| 163 |
+
:param get_validator: Callable that returns a :class:`.Validator` instance.
|
| 164 |
+
"""
|
| 165 |
+
|
| 166 |
+
def __init__(self, get_auto_suggest: Callable[[], AutoSuggest | None]) -> None:
|
| 167 |
+
self.get_auto_suggest = get_auto_suggest
|
| 168 |
+
|
| 169 |
+
def get_suggestion(self, buff: Buffer, document: Document) -> Suggestion | None:
|
| 170 |
+
auto_suggest = self.get_auto_suggest() or DummyAutoSuggest()
|
| 171 |
+
return auto_suggest.get_suggestion(buff, document)
|
| 172 |
+
|
| 173 |
+
async def get_suggestion_async(
|
| 174 |
+
self, buff: Buffer, document: Document
|
| 175 |
+
) -> Suggestion | None:
|
| 176 |
+
auto_suggest = self.get_auto_suggest() or DummyAutoSuggest()
|
| 177 |
+
return await auto_suggest.get_suggestion_async(buff, document)
|
lib/python3.12/site-packages/prompt_toolkit/buffer.py
ADDED
|
@@ -0,0 +1,2029 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data structures for the Buffer.
|
| 3 |
+
It holds the text, cursor position, history, etc...
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
import logging
|
| 10 |
+
import os
|
| 11 |
+
import re
|
| 12 |
+
import shlex
|
| 13 |
+
import shutil
|
| 14 |
+
import subprocess
|
| 15 |
+
import tempfile
|
| 16 |
+
from collections import deque
|
| 17 |
+
from enum import Enum
|
| 18 |
+
from functools import wraps
|
| 19 |
+
from typing import Any, Callable, Coroutine, Iterable, TypeVar, cast
|
| 20 |
+
|
| 21 |
+
from .application.current import get_app
|
| 22 |
+
from .application.run_in_terminal import run_in_terminal
|
| 23 |
+
from .auto_suggest import AutoSuggest, Suggestion
|
| 24 |
+
from .cache import FastDictCache
|
| 25 |
+
from .clipboard import ClipboardData
|
| 26 |
+
from .completion import (
|
| 27 |
+
CompleteEvent,
|
| 28 |
+
Completer,
|
| 29 |
+
Completion,
|
| 30 |
+
DummyCompleter,
|
| 31 |
+
get_common_complete_suffix,
|
| 32 |
+
)
|
| 33 |
+
from .document import Document
|
| 34 |
+
from .eventloop import aclosing
|
| 35 |
+
from .filters import FilterOrBool, to_filter
|
| 36 |
+
from .history import History, InMemoryHistory
|
| 37 |
+
from .search import SearchDirection, SearchState
|
| 38 |
+
from .selection import PasteMode, SelectionState, SelectionType
|
| 39 |
+
from .utils import Event, to_str
|
| 40 |
+
from .validation import ValidationError, Validator
|
| 41 |
+
|
| 42 |
+
__all__ = [
|
| 43 |
+
"EditReadOnlyBuffer",
|
| 44 |
+
"Buffer",
|
| 45 |
+
"CompletionState",
|
| 46 |
+
"indent",
|
| 47 |
+
"unindent",
|
| 48 |
+
"reshape_text",
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
logger = logging.getLogger(__name__)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class EditReadOnlyBuffer(Exception):
|
| 55 |
+
"Attempt editing of read-only :class:`.Buffer`."
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class ValidationState(Enum):
|
| 59 |
+
"The validation state of a buffer. This is set after the validation."
|
| 60 |
+
|
| 61 |
+
VALID = "VALID"
|
| 62 |
+
INVALID = "INVALID"
|
| 63 |
+
UNKNOWN = "UNKNOWN"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class CompletionState:
|
| 67 |
+
"""
|
| 68 |
+
Immutable class that contains a completion state.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
def __init__(
|
| 72 |
+
self,
|
| 73 |
+
original_document: Document,
|
| 74 |
+
completions: list[Completion] | None = None,
|
| 75 |
+
complete_index: int | None = None,
|
| 76 |
+
) -> None:
|
| 77 |
+
#: Document as it was when the completion started.
|
| 78 |
+
self.original_document = original_document
|
| 79 |
+
|
| 80 |
+
#: List of all the current Completion instances which are possible at
|
| 81 |
+
#: this point.
|
| 82 |
+
self.completions = completions or []
|
| 83 |
+
|
| 84 |
+
#: Position in the `completions` array.
|
| 85 |
+
#: This can be `None` to indicate "no completion", the original text.
|
| 86 |
+
self.complete_index = complete_index # Position in the `_completions` array.
|
| 87 |
+
|
| 88 |
+
def __repr__(self) -> str:
|
| 89 |
+
return f"{self.__class__.__name__}({self.original_document!r}, <{len(self.completions)!r}> completions, index={self.complete_index!r})"
|
| 90 |
+
|
| 91 |
+
def go_to_index(self, index: int | None) -> None:
|
| 92 |
+
"""
|
| 93 |
+
Create a new :class:`.CompletionState` object with the new index.
|
| 94 |
+
|
| 95 |
+
When `index` is `None` deselect the completion.
|
| 96 |
+
"""
|
| 97 |
+
if self.completions:
|
| 98 |
+
assert index is None or 0 <= index < len(self.completions)
|
| 99 |
+
self.complete_index = index
|
| 100 |
+
|
| 101 |
+
def new_text_and_position(self) -> tuple[str, int]:
|
| 102 |
+
"""
|
| 103 |
+
Return (new_text, new_cursor_position) for this completion.
|
| 104 |
+
"""
|
| 105 |
+
if self.complete_index is None:
|
| 106 |
+
return self.original_document.text, self.original_document.cursor_position
|
| 107 |
+
else:
|
| 108 |
+
original_text_before_cursor = self.original_document.text_before_cursor
|
| 109 |
+
original_text_after_cursor = self.original_document.text_after_cursor
|
| 110 |
+
|
| 111 |
+
c = self.completions[self.complete_index]
|
| 112 |
+
if c.start_position == 0:
|
| 113 |
+
before = original_text_before_cursor
|
| 114 |
+
else:
|
| 115 |
+
before = original_text_before_cursor[: c.start_position]
|
| 116 |
+
|
| 117 |
+
new_text = before + c.text + original_text_after_cursor
|
| 118 |
+
new_cursor_position = len(before) + len(c.text)
|
| 119 |
+
return new_text, new_cursor_position
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def current_completion(self) -> Completion | None:
|
| 123 |
+
"""
|
| 124 |
+
Return the current completion, or return `None` when no completion is
|
| 125 |
+
selected.
|
| 126 |
+
"""
|
| 127 |
+
if self.complete_index is not None:
|
| 128 |
+
return self.completions[self.complete_index]
|
| 129 |
+
return None
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
_QUOTED_WORDS_RE = re.compile(r"""(\s+|".*?"|'.*?')""")
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class YankNthArgState:
|
| 136 |
+
"""
|
| 137 |
+
For yank-last-arg/yank-nth-arg: Keep track of where we are in the history.
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
def __init__(
|
| 141 |
+
self, history_position: int = 0, n: int = -1, previous_inserted_word: str = ""
|
| 142 |
+
) -> None:
|
| 143 |
+
self.history_position = history_position
|
| 144 |
+
self.previous_inserted_word = previous_inserted_word
|
| 145 |
+
self.n = n
|
| 146 |
+
|
| 147 |
+
def __repr__(self) -> str:
|
| 148 |
+
return f"{self.__class__.__name__}(history_position={self.history_position!r}, n={self.n!r}, previous_inserted_word={self.previous_inserted_word!r})"
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
BufferEventHandler = Callable[["Buffer"], None]
|
| 152 |
+
BufferAcceptHandler = Callable[["Buffer"], bool]
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class Buffer:
|
| 156 |
+
"""
|
| 157 |
+
The core data structure that holds the text and cursor position of the
|
| 158 |
+
current input line and implements all text manipulations on top of it. It
|
| 159 |
+
also implements the history, undo stack and the completion state.
|
| 160 |
+
|
| 161 |
+
:param completer: :class:`~prompt_toolkit.completion.Completer` instance.
|
| 162 |
+
:param history: :class:`~prompt_toolkit.history.History` instance.
|
| 163 |
+
:param tempfile_suffix: The tempfile suffix (extension) to be used for the
|
| 164 |
+
"open in editor" function. For a Python REPL, this would be ".py", so
|
| 165 |
+
that the editor knows the syntax highlighting to use. This can also be
|
| 166 |
+
a callable that returns a string.
|
| 167 |
+
:param tempfile: For more advanced tempfile situations where you need
|
| 168 |
+
control over the subdirectories and filename. For a Git Commit Message,
|
| 169 |
+
this would be ".git/COMMIT_EDITMSG", so that the editor knows the syntax
|
| 170 |
+
highlighting to use. This can also be a callable that returns a string.
|
| 171 |
+
:param name: Name for this buffer. E.g. DEFAULT_BUFFER. This is mostly
|
| 172 |
+
useful for key bindings where we sometimes prefer to refer to a buffer
|
| 173 |
+
by their name instead of by reference.
|
| 174 |
+
:param accept_handler: Called when the buffer input is accepted. (Usually
|
| 175 |
+
when the user presses `enter`.) The accept handler receives this
|
| 176 |
+
`Buffer` as input and should return True when the buffer text should be
|
| 177 |
+
kept instead of calling reset.
|
| 178 |
+
|
| 179 |
+
In case of a `PromptSession` for instance, we want to keep the text,
|
| 180 |
+
because we will exit the application, and only reset it during the next
|
| 181 |
+
run.
|
| 182 |
+
:param max_number_of_completions: Never display more than this number of
|
| 183 |
+
completions, even when the completer can produce more (limited by
|
| 184 |
+
default to 10k for performance).
|
| 185 |
+
|
| 186 |
+
Events:
|
| 187 |
+
|
| 188 |
+
:param on_text_changed: When the buffer text changes. (Callable or None.)
|
| 189 |
+
:param on_text_insert: When new text is inserted. (Callable or None.)
|
| 190 |
+
:param on_cursor_position_changed: When the cursor moves. (Callable or None.)
|
| 191 |
+
:param on_completions_changed: When the completions were changed. (Callable or None.)
|
| 192 |
+
:param on_suggestion_set: When an auto-suggestion text has been set. (Callable or None.)
|
| 193 |
+
|
| 194 |
+
Filters:
|
| 195 |
+
|
| 196 |
+
:param complete_while_typing: :class:`~prompt_toolkit.filters.Filter`
|
| 197 |
+
or `bool`. Decide whether or not to do asynchronous autocompleting while
|
| 198 |
+
typing.
|
| 199 |
+
:param validate_while_typing: :class:`~prompt_toolkit.filters.Filter`
|
| 200 |
+
or `bool`. Decide whether or not to do asynchronous validation while
|
| 201 |
+
typing.
|
| 202 |
+
:param enable_history_search: :class:`~prompt_toolkit.filters.Filter` or
|
| 203 |
+
`bool` to indicate when up-arrow partial string matching is enabled. It
|
| 204 |
+
is advised to not enable this at the same time as
|
| 205 |
+
`complete_while_typing`, because when there is an autocompletion found,
|
| 206 |
+
the up arrows usually browse through the completions, rather than
|
| 207 |
+
through the history.
|
| 208 |
+
:param read_only: :class:`~prompt_toolkit.filters.Filter`. When True,
|
| 209 |
+
changes will not be allowed.
|
| 210 |
+
:param multiline: :class:`~prompt_toolkit.filters.Filter` or `bool`. When
|
| 211 |
+
not set, pressing `Enter` will call the `accept_handler`. Otherwise,
|
| 212 |
+
pressing `Esc-Enter` is required.
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
def __init__(
|
| 216 |
+
self,
|
| 217 |
+
completer: Completer | None = None,
|
| 218 |
+
auto_suggest: AutoSuggest | None = None,
|
| 219 |
+
history: History | None = None,
|
| 220 |
+
validator: Validator | None = None,
|
| 221 |
+
tempfile_suffix: str | Callable[[], str] = "",
|
| 222 |
+
tempfile: str | Callable[[], str] = "",
|
| 223 |
+
name: str = "",
|
| 224 |
+
complete_while_typing: FilterOrBool = False,
|
| 225 |
+
validate_while_typing: FilterOrBool = False,
|
| 226 |
+
enable_history_search: FilterOrBool = False,
|
| 227 |
+
document: Document | None = None,
|
| 228 |
+
accept_handler: BufferAcceptHandler | None = None,
|
| 229 |
+
read_only: FilterOrBool = False,
|
| 230 |
+
multiline: FilterOrBool = True,
|
| 231 |
+
max_number_of_completions: int = 10000,
|
| 232 |
+
on_text_changed: BufferEventHandler | None = None,
|
| 233 |
+
on_text_insert: BufferEventHandler | None = None,
|
| 234 |
+
on_cursor_position_changed: BufferEventHandler | None = None,
|
| 235 |
+
on_completions_changed: BufferEventHandler | None = None,
|
| 236 |
+
on_suggestion_set: BufferEventHandler | None = None,
|
| 237 |
+
) -> None:
|
| 238 |
+
# Accept both filters and booleans as input.
|
| 239 |
+
enable_history_search = to_filter(enable_history_search)
|
| 240 |
+
complete_while_typing = to_filter(complete_while_typing)
|
| 241 |
+
validate_while_typing = to_filter(validate_while_typing)
|
| 242 |
+
read_only = to_filter(read_only)
|
| 243 |
+
multiline = to_filter(multiline)
|
| 244 |
+
|
| 245 |
+
self.completer = completer or DummyCompleter()
|
| 246 |
+
self.auto_suggest = auto_suggest
|
| 247 |
+
self.validator = validator
|
| 248 |
+
self.tempfile_suffix = tempfile_suffix
|
| 249 |
+
self.tempfile = tempfile
|
| 250 |
+
self.name = name
|
| 251 |
+
self.accept_handler = accept_handler
|
| 252 |
+
|
| 253 |
+
# Filters. (Usually, used by the key bindings to drive the buffer.)
|
| 254 |
+
self.complete_while_typing = complete_while_typing
|
| 255 |
+
self.validate_while_typing = validate_while_typing
|
| 256 |
+
self.enable_history_search = enable_history_search
|
| 257 |
+
self.read_only = read_only
|
| 258 |
+
self.multiline = multiline
|
| 259 |
+
self.max_number_of_completions = max_number_of_completions
|
| 260 |
+
|
| 261 |
+
# Text width. (For wrapping, used by the Vi 'gq' operator.)
|
| 262 |
+
self.text_width = 0
|
| 263 |
+
|
| 264 |
+
#: The command buffer history.
|
| 265 |
+
# Note that we shouldn't use a lazy 'or' here. bool(history) could be
|
| 266 |
+
# False when empty.
|
| 267 |
+
self.history = InMemoryHistory() if history is None else history
|
| 268 |
+
|
| 269 |
+
self.__cursor_position = 0
|
| 270 |
+
|
| 271 |
+
# Events
|
| 272 |
+
self.on_text_changed: Event[Buffer] = Event(self, on_text_changed)
|
| 273 |
+
self.on_text_insert: Event[Buffer] = Event(self, on_text_insert)
|
| 274 |
+
self.on_cursor_position_changed: Event[Buffer] = Event(
|
| 275 |
+
self, on_cursor_position_changed
|
| 276 |
+
)
|
| 277 |
+
self.on_completions_changed: Event[Buffer] = Event(self, on_completions_changed)
|
| 278 |
+
self.on_suggestion_set: Event[Buffer] = Event(self, on_suggestion_set)
|
| 279 |
+
|
| 280 |
+
# Document cache. (Avoid creating new Document instances.)
|
| 281 |
+
self._document_cache: FastDictCache[
|
| 282 |
+
tuple[str, int, SelectionState | None], Document
|
| 283 |
+
] = FastDictCache(Document, size=10)
|
| 284 |
+
|
| 285 |
+
# Create completer / auto suggestion / validation coroutines.
|
| 286 |
+
self._async_suggester = self._create_auto_suggest_coroutine()
|
| 287 |
+
self._async_completer = self._create_completer_coroutine()
|
| 288 |
+
self._async_validator = self._create_auto_validate_coroutine()
|
| 289 |
+
|
| 290 |
+
# Asyncio task for populating the history.
|
| 291 |
+
self._load_history_task: asyncio.Future[None] | None = None
|
| 292 |
+
|
| 293 |
+
# Reset other attributes.
|
| 294 |
+
self.reset(document=document)
|
| 295 |
+
|
| 296 |
+
def __repr__(self) -> str:
|
| 297 |
+
if len(self.text) < 15:
|
| 298 |
+
text = self.text
|
| 299 |
+
else:
|
| 300 |
+
text = self.text[:12] + "..."
|
| 301 |
+
|
| 302 |
+
return f"<Buffer(name={self.name!r}, text={text!r}) at {id(self)!r}>"
|
| 303 |
+
|
| 304 |
+
def reset(
|
| 305 |
+
self, document: Document | None = None, append_to_history: bool = False
|
| 306 |
+
) -> None:
|
| 307 |
+
"""
|
| 308 |
+
:param append_to_history: Append current input to history first.
|
| 309 |
+
"""
|
| 310 |
+
if append_to_history:
|
| 311 |
+
self.append_to_history()
|
| 312 |
+
|
| 313 |
+
document = document or Document()
|
| 314 |
+
|
| 315 |
+
self.__cursor_position = document.cursor_position
|
| 316 |
+
|
| 317 |
+
# `ValidationError` instance. (Will be set when the input is wrong.)
|
| 318 |
+
self.validation_error: ValidationError | None = None
|
| 319 |
+
self.validation_state: ValidationState | None = ValidationState.UNKNOWN
|
| 320 |
+
|
| 321 |
+
# State of the selection.
|
| 322 |
+
self.selection_state: SelectionState | None = None
|
| 323 |
+
|
| 324 |
+
# Multiple cursor mode. (When we press 'I' or 'A' in visual-block mode,
|
| 325 |
+
# we can insert text on multiple lines at once. This is implemented by
|
| 326 |
+
# using multiple cursors.)
|
| 327 |
+
self.multiple_cursor_positions: list[int] = []
|
| 328 |
+
|
| 329 |
+
# When doing consecutive up/down movements, prefer to stay at this column.
|
| 330 |
+
self.preferred_column: int | None = None
|
| 331 |
+
|
| 332 |
+
# State of complete browser
|
| 333 |
+
# For interactive completion through Ctrl-N/Ctrl-P.
|
| 334 |
+
self.complete_state: CompletionState | None = None
|
| 335 |
+
|
| 336 |
+
# State of Emacs yank-nth-arg completion.
|
| 337 |
+
self.yank_nth_arg_state: YankNthArgState | None = None # for yank-nth-arg.
|
| 338 |
+
|
| 339 |
+
# Remember the document that we had *right before* the last paste
|
| 340 |
+
# operation. This is used for rotating through the kill ring.
|
| 341 |
+
self.document_before_paste: Document | None = None
|
| 342 |
+
|
| 343 |
+
# Current suggestion.
|
| 344 |
+
self.suggestion: Suggestion | None = None
|
| 345 |
+
|
| 346 |
+
# The history search text. (Used for filtering the history when we
|
| 347 |
+
# browse through it.)
|
| 348 |
+
self.history_search_text: str | None = None
|
| 349 |
+
|
| 350 |
+
# Undo/redo stacks (stack of `(text, cursor_position)`).
|
| 351 |
+
self._undo_stack: list[tuple[str, int]] = []
|
| 352 |
+
self._redo_stack: list[tuple[str, int]] = []
|
| 353 |
+
|
| 354 |
+
# Cancel history loader. If history loading was still ongoing.
|
| 355 |
+
# Cancel the `_load_history_task`, so that next repaint of the
|
| 356 |
+
# `BufferControl` we will repopulate it.
|
| 357 |
+
if self._load_history_task is not None:
|
| 358 |
+
self._load_history_task.cancel()
|
| 359 |
+
self._load_history_task = None
|
| 360 |
+
|
| 361 |
+
#: The working lines. Similar to history, except that this can be
|
| 362 |
+
#: modified. The user can press arrow_up and edit previous entries.
|
| 363 |
+
#: Ctrl-C should reset this, and copy the whole history back in here.
|
| 364 |
+
#: Enter should process the current command and append to the real
|
| 365 |
+
#: history.
|
| 366 |
+
self._working_lines: deque[str] = deque([document.text])
|
| 367 |
+
self.__working_index = 0
|
| 368 |
+
|
| 369 |
+
def load_history_if_not_yet_loaded(self) -> None:
|
| 370 |
+
"""
|
| 371 |
+
Create task for populating the buffer history (if not yet done).
|
| 372 |
+
|
| 373 |
+
Note::
|
| 374 |
+
|
| 375 |
+
This needs to be called from within the event loop of the
|
| 376 |
+
application, because history loading is async, and we need to be
|
| 377 |
+
sure the right event loop is active. Therefor, we call this method
|
| 378 |
+
in the `BufferControl.create_content`.
|
| 379 |
+
|
| 380 |
+
There are situations where prompt_toolkit applications are created
|
| 381 |
+
in one thread, but will later run in a different thread (Ptpython
|
| 382 |
+
is one example. The REPL runs in a separate thread, in order to
|
| 383 |
+
prevent interfering with a potential different event loop in the
|
| 384 |
+
main thread. The REPL UI however is still created in the main
|
| 385 |
+
thread.) We could decide to not support creating prompt_toolkit
|
| 386 |
+
objects in one thread and running the application in a different
|
| 387 |
+
thread, but history loading is the only place where it matters, and
|
| 388 |
+
this solves it.
|
| 389 |
+
"""
|
| 390 |
+
if self._load_history_task is None:
|
| 391 |
+
|
| 392 |
+
async def load_history() -> None:
|
| 393 |
+
async for item in self.history.load():
|
| 394 |
+
self._working_lines.appendleft(item)
|
| 395 |
+
self.__working_index += 1
|
| 396 |
+
|
| 397 |
+
self._load_history_task = get_app().create_background_task(load_history())
|
| 398 |
+
|
| 399 |
+
def load_history_done(f: asyncio.Future[None]) -> None:
|
| 400 |
+
"""
|
| 401 |
+
Handle `load_history` result when either done, cancelled, or
|
| 402 |
+
when an exception was raised.
|
| 403 |
+
"""
|
| 404 |
+
try:
|
| 405 |
+
f.result()
|
| 406 |
+
except asyncio.CancelledError:
|
| 407 |
+
# Ignore cancellation. But handle it, so that we don't get
|
| 408 |
+
# this traceback.
|
| 409 |
+
pass
|
| 410 |
+
except GeneratorExit:
|
| 411 |
+
# Probably not needed, but we had situations where
|
| 412 |
+
# `GeneratorExit` was raised in `load_history` during
|
| 413 |
+
# cancellation.
|
| 414 |
+
pass
|
| 415 |
+
except BaseException:
|
| 416 |
+
# Log error if something goes wrong. (We don't have a
|
| 417 |
+
# caller to which we can propagate this exception.)
|
| 418 |
+
logger.exception("Loading history failed")
|
| 419 |
+
|
| 420 |
+
self._load_history_task.add_done_callback(load_history_done)
|
| 421 |
+
|
| 422 |
+
# <getters/setters>
|
| 423 |
+
|
| 424 |
+
def _set_text(self, value: str) -> bool:
|
| 425 |
+
"""set text at current working_index. Return whether it changed."""
|
| 426 |
+
working_index = self.working_index
|
| 427 |
+
working_lines = self._working_lines
|
| 428 |
+
|
| 429 |
+
original_value = working_lines[working_index]
|
| 430 |
+
working_lines[working_index] = value
|
| 431 |
+
|
| 432 |
+
# Return True when this text has been changed.
|
| 433 |
+
if len(value) != len(original_value):
|
| 434 |
+
# For Python 2, it seems that when two strings have a different
|
| 435 |
+
# length and one is a prefix of the other, Python still scans
|
| 436 |
+
# character by character to see whether the strings are different.
|
| 437 |
+
# (Some benchmarking showed significant differences for big
|
| 438 |
+
# documents. >100,000 of lines.)
|
| 439 |
+
return True
|
| 440 |
+
elif value != original_value:
|
| 441 |
+
return True
|
| 442 |
+
return False
|
| 443 |
+
|
| 444 |
+
def _set_cursor_position(self, value: int) -> bool:
|
| 445 |
+
"""Set cursor position. Return whether it changed."""
|
| 446 |
+
original_position = self.__cursor_position
|
| 447 |
+
self.__cursor_position = max(0, value)
|
| 448 |
+
|
| 449 |
+
return self.__cursor_position != original_position
|
| 450 |
+
|
| 451 |
+
@property
|
| 452 |
+
def text(self) -> str:
|
| 453 |
+
return self._working_lines[self.working_index]
|
| 454 |
+
|
| 455 |
+
@text.setter
|
| 456 |
+
def text(self, value: str) -> None:
|
| 457 |
+
"""
|
| 458 |
+
Setting text. (When doing this, make sure that the cursor_position is
|
| 459 |
+
valid for this text. text/cursor_position should be consistent at any time,
|
| 460 |
+
otherwise set a Document instead.)
|
| 461 |
+
"""
|
| 462 |
+
# Ensure cursor position remains within the size of the text.
|
| 463 |
+
if self.cursor_position > len(value):
|
| 464 |
+
self.cursor_position = len(value)
|
| 465 |
+
|
| 466 |
+
# Don't allow editing of read-only buffers.
|
| 467 |
+
if self.read_only():
|
| 468 |
+
raise EditReadOnlyBuffer()
|
| 469 |
+
|
| 470 |
+
changed = self._set_text(value)
|
| 471 |
+
|
| 472 |
+
if changed:
|
| 473 |
+
self._text_changed()
|
| 474 |
+
|
| 475 |
+
# Reset history search text.
|
| 476 |
+
# (Note that this doesn't need to happen when working_index
|
| 477 |
+
# changes, which is when we traverse the history. That's why we
|
| 478 |
+
# don't do this in `self._text_changed`.)
|
| 479 |
+
self.history_search_text = None
|
| 480 |
+
|
| 481 |
+
@property
|
| 482 |
+
def cursor_position(self) -> int:
|
| 483 |
+
return self.__cursor_position
|
| 484 |
+
|
| 485 |
+
@cursor_position.setter
|
| 486 |
+
def cursor_position(self, value: int) -> None:
|
| 487 |
+
"""
|
| 488 |
+
Setting cursor position.
|
| 489 |
+
"""
|
| 490 |
+
assert isinstance(value, int)
|
| 491 |
+
|
| 492 |
+
# Ensure cursor position is within the size of the text.
|
| 493 |
+
if value > len(self.text):
|
| 494 |
+
value = len(self.text)
|
| 495 |
+
if value < 0:
|
| 496 |
+
value = 0
|
| 497 |
+
|
| 498 |
+
changed = self._set_cursor_position(value)
|
| 499 |
+
|
| 500 |
+
if changed:
|
| 501 |
+
self._cursor_position_changed()
|
| 502 |
+
|
| 503 |
+
@property
|
| 504 |
+
def working_index(self) -> int:
|
| 505 |
+
return self.__working_index
|
| 506 |
+
|
| 507 |
+
@working_index.setter
|
| 508 |
+
def working_index(self, value: int) -> None:
|
| 509 |
+
if self.__working_index != value:
|
| 510 |
+
self.__working_index = value
|
| 511 |
+
# Make sure to reset the cursor position, otherwise we end up in
|
| 512 |
+
# situations where the cursor position is out of the bounds of the
|
| 513 |
+
# text.
|
| 514 |
+
self.cursor_position = 0
|
| 515 |
+
self._text_changed()
|
| 516 |
+
|
| 517 |
+
def _text_changed(self) -> None:
|
| 518 |
+
# Remove any validation errors and complete state.
|
| 519 |
+
self.validation_error = None
|
| 520 |
+
self.validation_state = ValidationState.UNKNOWN
|
| 521 |
+
self.complete_state = None
|
| 522 |
+
self.yank_nth_arg_state = None
|
| 523 |
+
self.document_before_paste = None
|
| 524 |
+
self.selection_state = None
|
| 525 |
+
self.suggestion = None
|
| 526 |
+
self.preferred_column = None
|
| 527 |
+
|
| 528 |
+
# fire 'on_text_changed' event.
|
| 529 |
+
self.on_text_changed.fire()
|
| 530 |
+
|
| 531 |
+
# Input validation.
|
| 532 |
+
# (This happens on all change events, unlike auto completion, also when
|
| 533 |
+
# deleting text.)
|
| 534 |
+
if self.validator and self.validate_while_typing():
|
| 535 |
+
get_app().create_background_task(self._async_validator())
|
| 536 |
+
|
| 537 |
+
def _cursor_position_changed(self) -> None:
|
| 538 |
+
# Remove any complete state.
|
| 539 |
+
# (Input validation should only be undone when the cursor position
|
| 540 |
+
# changes.)
|
| 541 |
+
self.complete_state = None
|
| 542 |
+
self.yank_nth_arg_state = None
|
| 543 |
+
self.document_before_paste = None
|
| 544 |
+
|
| 545 |
+
# Unset preferred_column. (Will be set after the cursor movement, if
|
| 546 |
+
# required.)
|
| 547 |
+
self.preferred_column = None
|
| 548 |
+
|
| 549 |
+
# Note that the cursor position can change if we have a selection the
|
| 550 |
+
# new position of the cursor determines the end of the selection.
|
| 551 |
+
|
| 552 |
+
# fire 'on_cursor_position_changed' event.
|
| 553 |
+
self.on_cursor_position_changed.fire()
|
| 554 |
+
|
| 555 |
+
@property
|
| 556 |
+
def document(self) -> Document:
|
| 557 |
+
"""
|
| 558 |
+
Return :class:`~prompt_toolkit.document.Document` instance from the
|
| 559 |
+
current text, cursor position and selection state.
|
| 560 |
+
"""
|
| 561 |
+
return self._document_cache[
|
| 562 |
+
self.text, self.cursor_position, self.selection_state
|
| 563 |
+
]
|
| 564 |
+
|
| 565 |
+
@document.setter
|
| 566 |
+
def document(self, value: Document) -> None:
|
| 567 |
+
"""
|
| 568 |
+
Set :class:`~prompt_toolkit.document.Document` instance.
|
| 569 |
+
|
| 570 |
+
This will set both the text and cursor position at the same time, but
|
| 571 |
+
atomically. (Change events will be triggered only after both have been set.)
|
| 572 |
+
"""
|
| 573 |
+
self.set_document(value)
|
| 574 |
+
|
| 575 |
+
def set_document(self, value: Document, bypass_readonly: bool = False) -> None:
|
| 576 |
+
"""
|
| 577 |
+
Set :class:`~prompt_toolkit.document.Document` instance. Like the
|
| 578 |
+
``document`` property, but accept an ``bypass_readonly`` argument.
|
| 579 |
+
|
| 580 |
+
:param bypass_readonly: When True, don't raise an
|
| 581 |
+
:class:`.EditReadOnlyBuffer` exception, even
|
| 582 |
+
when the buffer is read-only.
|
| 583 |
+
|
| 584 |
+
.. warning::
|
| 585 |
+
|
| 586 |
+
When this buffer is read-only and `bypass_readonly` was not passed,
|
| 587 |
+
the `EditReadOnlyBuffer` exception will be caught by the
|
| 588 |
+
`KeyProcessor` and is silently suppressed. This is important to
|
| 589 |
+
keep in mind when writing key bindings, because it won't do what
|
| 590 |
+
you expect, and there won't be a stack trace. Use try/finally
|
| 591 |
+
around this function if you need some cleanup code.
|
| 592 |
+
"""
|
| 593 |
+
# Don't allow editing of read-only buffers.
|
| 594 |
+
if not bypass_readonly and self.read_only():
|
| 595 |
+
raise EditReadOnlyBuffer()
|
| 596 |
+
|
| 597 |
+
# Set text and cursor position first.
|
| 598 |
+
text_changed = self._set_text(value.text)
|
| 599 |
+
cursor_position_changed = self._set_cursor_position(value.cursor_position)
|
| 600 |
+
|
| 601 |
+
# Now handle change events. (We do this when text/cursor position is
|
| 602 |
+
# both set and consistent.)
|
| 603 |
+
if text_changed:
|
| 604 |
+
self._text_changed()
|
| 605 |
+
self.history_search_text = None
|
| 606 |
+
|
| 607 |
+
if cursor_position_changed:
|
| 608 |
+
self._cursor_position_changed()
|
| 609 |
+
|
| 610 |
+
@property
|
| 611 |
+
def is_returnable(self) -> bool:
|
| 612 |
+
"""
|
| 613 |
+
True when there is something handling accept.
|
| 614 |
+
"""
|
| 615 |
+
return bool(self.accept_handler)
|
| 616 |
+
|
| 617 |
+
# End of <getters/setters>
|
| 618 |
+
|
| 619 |
+
def save_to_undo_stack(self, clear_redo_stack: bool = True) -> None:
|
| 620 |
+
"""
|
| 621 |
+
Safe current state (input text and cursor position), so that we can
|
| 622 |
+
restore it by calling undo.
|
| 623 |
+
"""
|
| 624 |
+
# Safe if the text is different from the text at the top of the stack
|
| 625 |
+
# is different. If the text is the same, just update the cursor position.
|
| 626 |
+
if self._undo_stack and self._undo_stack[-1][0] == self.text:
|
| 627 |
+
self._undo_stack[-1] = (self._undo_stack[-1][0], self.cursor_position)
|
| 628 |
+
else:
|
| 629 |
+
self._undo_stack.append((self.text, self.cursor_position))
|
| 630 |
+
|
| 631 |
+
# Saving anything to the undo stack, clears the redo stack.
|
| 632 |
+
if clear_redo_stack:
|
| 633 |
+
self._redo_stack = []
|
| 634 |
+
|
| 635 |
+
def transform_lines(
|
| 636 |
+
self,
|
| 637 |
+
line_index_iterator: Iterable[int],
|
| 638 |
+
transform_callback: Callable[[str], str],
|
| 639 |
+
) -> str:
|
| 640 |
+
"""
|
| 641 |
+
Transforms the text on a range of lines.
|
| 642 |
+
When the iterator yield an index not in the range of lines that the
|
| 643 |
+
document contains, it skips them silently.
|
| 644 |
+
|
| 645 |
+
To uppercase some lines::
|
| 646 |
+
|
| 647 |
+
new_text = transform_lines(range(5,10), lambda text: text.upper())
|
| 648 |
+
|
| 649 |
+
:param line_index_iterator: Iterator of line numbers (int)
|
| 650 |
+
:param transform_callback: callable that takes the original text of a
|
| 651 |
+
line, and return the new text for this line.
|
| 652 |
+
|
| 653 |
+
:returns: The new text.
|
| 654 |
+
"""
|
| 655 |
+
# Split lines
|
| 656 |
+
lines = self.text.split("\n")
|
| 657 |
+
|
| 658 |
+
# Apply transformation
|
| 659 |
+
for index in line_index_iterator:
|
| 660 |
+
try:
|
| 661 |
+
lines[index] = transform_callback(lines[index])
|
| 662 |
+
except IndexError:
|
| 663 |
+
pass
|
| 664 |
+
|
| 665 |
+
return "\n".join(lines)
|
| 666 |
+
|
| 667 |
+
def transform_current_line(self, transform_callback: Callable[[str], str]) -> None:
|
| 668 |
+
"""
|
| 669 |
+
Apply the given transformation function to the current line.
|
| 670 |
+
|
| 671 |
+
:param transform_callback: callable that takes a string and return a new string.
|
| 672 |
+
"""
|
| 673 |
+
document = self.document
|
| 674 |
+
a = document.cursor_position + document.get_start_of_line_position()
|
| 675 |
+
b = document.cursor_position + document.get_end_of_line_position()
|
| 676 |
+
self.text = (
|
| 677 |
+
document.text[:a]
|
| 678 |
+
+ transform_callback(document.text[a:b])
|
| 679 |
+
+ document.text[b:]
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
def transform_region(
|
| 683 |
+
self, from_: int, to: int, transform_callback: Callable[[str], str]
|
| 684 |
+
) -> None:
|
| 685 |
+
"""
|
| 686 |
+
Transform a part of the input string.
|
| 687 |
+
|
| 688 |
+
:param from_: (int) start position.
|
| 689 |
+
:param to: (int) end position.
|
| 690 |
+
:param transform_callback: Callable which accepts a string and returns
|
| 691 |
+
the transformed string.
|
| 692 |
+
"""
|
| 693 |
+
assert from_ < to
|
| 694 |
+
|
| 695 |
+
self.text = "".join(
|
| 696 |
+
[
|
| 697 |
+
self.text[:from_]
|
| 698 |
+
+ transform_callback(self.text[from_:to])
|
| 699 |
+
+ self.text[to:]
|
| 700 |
+
]
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
def cursor_left(self, count: int = 1) -> None:
|
| 704 |
+
self.cursor_position += self.document.get_cursor_left_position(count=count)
|
| 705 |
+
|
| 706 |
+
def cursor_right(self, count: int = 1) -> None:
|
| 707 |
+
self.cursor_position += self.document.get_cursor_right_position(count=count)
|
| 708 |
+
|
| 709 |
+
def cursor_up(self, count: int = 1) -> None:
|
| 710 |
+
"""(for multiline edit). Move cursor to the previous line."""
|
| 711 |
+
original_column = self.preferred_column or self.document.cursor_position_col
|
| 712 |
+
self.cursor_position += self.document.get_cursor_up_position(
|
| 713 |
+
count=count, preferred_column=original_column
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
# Remember the original column for the next up/down movement.
|
| 717 |
+
self.preferred_column = original_column
|
| 718 |
+
|
| 719 |
+
def cursor_down(self, count: int = 1) -> None:
|
| 720 |
+
"""(for multiline edit). Move cursor to the next line."""
|
| 721 |
+
original_column = self.preferred_column or self.document.cursor_position_col
|
| 722 |
+
self.cursor_position += self.document.get_cursor_down_position(
|
| 723 |
+
count=count, preferred_column=original_column
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
# Remember the original column for the next up/down movement.
|
| 727 |
+
self.preferred_column = original_column
|
| 728 |
+
|
| 729 |
+
def auto_up(
|
| 730 |
+
self, count: int = 1, go_to_start_of_line_if_history_changes: bool = False
|
| 731 |
+
) -> None:
|
| 732 |
+
"""
|
| 733 |
+
If we're not on the first line (of a multiline input) go a line up,
|
| 734 |
+
otherwise go back in history. (If nothing is selected.)
|
| 735 |
+
"""
|
| 736 |
+
if self.complete_state:
|
| 737 |
+
self.complete_previous(count=count)
|
| 738 |
+
elif self.document.cursor_position_row > 0:
|
| 739 |
+
self.cursor_up(count=count)
|
| 740 |
+
elif not self.selection_state:
|
| 741 |
+
self.history_backward(count=count)
|
| 742 |
+
|
| 743 |
+
# Go to the start of the line?
|
| 744 |
+
if go_to_start_of_line_if_history_changes:
|
| 745 |
+
self.cursor_position += self.document.get_start_of_line_position()
|
| 746 |
+
|
| 747 |
+
def auto_down(
|
| 748 |
+
self, count: int = 1, go_to_start_of_line_if_history_changes: bool = False
|
| 749 |
+
) -> None:
|
| 750 |
+
"""
|
| 751 |
+
If we're not on the last line (of a multiline input) go a line down,
|
| 752 |
+
otherwise go forward in history. (If nothing is selected.)
|
| 753 |
+
"""
|
| 754 |
+
if self.complete_state:
|
| 755 |
+
self.complete_next(count=count)
|
| 756 |
+
elif self.document.cursor_position_row < self.document.line_count - 1:
|
| 757 |
+
self.cursor_down(count=count)
|
| 758 |
+
elif not self.selection_state:
|
| 759 |
+
self.history_forward(count=count)
|
| 760 |
+
|
| 761 |
+
# Go to the start of the line?
|
| 762 |
+
if go_to_start_of_line_if_history_changes:
|
| 763 |
+
self.cursor_position += self.document.get_start_of_line_position()
|
| 764 |
+
|
| 765 |
+
def delete_before_cursor(self, count: int = 1) -> str:
|
| 766 |
+
"""
|
| 767 |
+
Delete specified number of characters before cursor and return the
|
| 768 |
+
deleted text.
|
| 769 |
+
"""
|
| 770 |
+
assert count >= 0
|
| 771 |
+
deleted = ""
|
| 772 |
+
|
| 773 |
+
if self.cursor_position > 0:
|
| 774 |
+
deleted = self.text[self.cursor_position - count : self.cursor_position]
|
| 775 |
+
|
| 776 |
+
new_text = (
|
| 777 |
+
self.text[: self.cursor_position - count]
|
| 778 |
+
+ self.text[self.cursor_position :]
|
| 779 |
+
)
|
| 780 |
+
new_cursor_position = self.cursor_position - len(deleted)
|
| 781 |
+
|
| 782 |
+
# Set new Document atomically.
|
| 783 |
+
self.document = Document(new_text, new_cursor_position)
|
| 784 |
+
|
| 785 |
+
return deleted
|
| 786 |
+
|
| 787 |
+
def delete(self, count: int = 1) -> str:
|
| 788 |
+
"""
|
| 789 |
+
Delete specified number of characters and Return the deleted text.
|
| 790 |
+
"""
|
| 791 |
+
if self.cursor_position < len(self.text):
|
| 792 |
+
deleted = self.document.text_after_cursor[:count]
|
| 793 |
+
self.text = (
|
| 794 |
+
self.text[: self.cursor_position]
|
| 795 |
+
+ self.text[self.cursor_position + len(deleted) :]
|
| 796 |
+
)
|
| 797 |
+
return deleted
|
| 798 |
+
else:
|
| 799 |
+
return ""
|
| 800 |
+
|
| 801 |
+
def join_next_line(self, separator: str = " ") -> None:
|
| 802 |
+
"""
|
| 803 |
+
Join the next line to the current one by deleting the line ending after
|
| 804 |
+
the current line.
|
| 805 |
+
"""
|
| 806 |
+
if not self.document.on_last_line:
|
| 807 |
+
self.cursor_position += self.document.get_end_of_line_position()
|
| 808 |
+
self.delete()
|
| 809 |
+
|
| 810 |
+
# Remove spaces.
|
| 811 |
+
self.text = (
|
| 812 |
+
self.document.text_before_cursor
|
| 813 |
+
+ separator
|
| 814 |
+
+ self.document.text_after_cursor.lstrip(" ")
|
| 815 |
+
)
|
| 816 |
+
|
| 817 |
+
def join_selected_lines(self, separator: str = " ") -> None:
|
| 818 |
+
"""
|
| 819 |
+
Join the selected lines.
|
| 820 |
+
"""
|
| 821 |
+
assert self.selection_state
|
| 822 |
+
|
| 823 |
+
# Get lines.
|
| 824 |
+
from_, to = sorted(
|
| 825 |
+
[self.cursor_position, self.selection_state.original_cursor_position]
|
| 826 |
+
)
|
| 827 |
+
|
| 828 |
+
before = self.text[:from_]
|
| 829 |
+
lines = self.text[from_:to].splitlines()
|
| 830 |
+
after = self.text[to:]
|
| 831 |
+
|
| 832 |
+
# Replace leading spaces with just one space.
|
| 833 |
+
lines = [l.lstrip(" ") + separator for l in lines]
|
| 834 |
+
|
| 835 |
+
# Set new document.
|
| 836 |
+
self.document = Document(
|
| 837 |
+
text=before + "".join(lines) + after,
|
| 838 |
+
cursor_position=len(before + "".join(lines[:-1])) - 1,
|
| 839 |
+
)
|
| 840 |
+
|
| 841 |
+
def swap_characters_before_cursor(self) -> None:
|
| 842 |
+
"""
|
| 843 |
+
Swap the last two characters before the cursor.
|
| 844 |
+
"""
|
| 845 |
+
pos = self.cursor_position
|
| 846 |
+
|
| 847 |
+
if pos >= 2:
|
| 848 |
+
a = self.text[pos - 2]
|
| 849 |
+
b = self.text[pos - 1]
|
| 850 |
+
|
| 851 |
+
self.text = self.text[: pos - 2] + b + a + self.text[pos:]
|
| 852 |
+
|
| 853 |
+
def go_to_history(self, index: int) -> None:
|
| 854 |
+
"""
|
| 855 |
+
Go to this item in the history.
|
| 856 |
+
"""
|
| 857 |
+
if index < len(self._working_lines):
|
| 858 |
+
self.working_index = index
|
| 859 |
+
self.cursor_position = len(self.text)
|
| 860 |
+
|
| 861 |
+
def complete_next(self, count: int = 1, disable_wrap_around: bool = False) -> None:
|
| 862 |
+
"""
|
| 863 |
+
Browse to the next completions.
|
| 864 |
+
(Does nothing if there are no completion.)
|
| 865 |
+
"""
|
| 866 |
+
index: int | None
|
| 867 |
+
|
| 868 |
+
if self.complete_state:
|
| 869 |
+
completions_count = len(self.complete_state.completions)
|
| 870 |
+
|
| 871 |
+
if self.complete_state.complete_index is None:
|
| 872 |
+
index = 0
|
| 873 |
+
elif self.complete_state.complete_index == completions_count - 1:
|
| 874 |
+
index = None
|
| 875 |
+
|
| 876 |
+
if disable_wrap_around:
|
| 877 |
+
return
|
| 878 |
+
else:
|
| 879 |
+
index = min(
|
| 880 |
+
completions_count - 1, self.complete_state.complete_index + count
|
| 881 |
+
)
|
| 882 |
+
self.go_to_completion(index)
|
| 883 |
+
|
| 884 |
+
def complete_previous(
|
| 885 |
+
self, count: int = 1, disable_wrap_around: bool = False
|
| 886 |
+
) -> None:
|
| 887 |
+
"""
|
| 888 |
+
Browse to the previous completions.
|
| 889 |
+
(Does nothing if there are no completion.)
|
| 890 |
+
"""
|
| 891 |
+
index: int | None
|
| 892 |
+
|
| 893 |
+
if self.complete_state:
|
| 894 |
+
if self.complete_state.complete_index == 0:
|
| 895 |
+
index = None
|
| 896 |
+
|
| 897 |
+
if disable_wrap_around:
|
| 898 |
+
return
|
| 899 |
+
elif self.complete_state.complete_index is None:
|
| 900 |
+
index = len(self.complete_state.completions) - 1
|
| 901 |
+
else:
|
| 902 |
+
index = max(0, self.complete_state.complete_index - count)
|
| 903 |
+
|
| 904 |
+
self.go_to_completion(index)
|
| 905 |
+
|
| 906 |
+
def cancel_completion(self) -> None:
|
| 907 |
+
"""
|
| 908 |
+
Cancel completion, go back to the original text.
|
| 909 |
+
"""
|
| 910 |
+
if self.complete_state:
|
| 911 |
+
self.go_to_completion(None)
|
| 912 |
+
self.complete_state = None
|
| 913 |
+
|
| 914 |
+
def _set_completions(self, completions: list[Completion]) -> CompletionState:
|
| 915 |
+
"""
|
| 916 |
+
Start completions. (Generate list of completions and initialize.)
|
| 917 |
+
|
| 918 |
+
By default, no completion will be selected.
|
| 919 |
+
"""
|
| 920 |
+
self.complete_state = CompletionState(
|
| 921 |
+
original_document=self.document, completions=completions
|
| 922 |
+
)
|
| 923 |
+
|
| 924 |
+
# Trigger event. This should eventually invalidate the layout.
|
| 925 |
+
self.on_completions_changed.fire()
|
| 926 |
+
|
| 927 |
+
return self.complete_state
|
| 928 |
+
|
| 929 |
+
def start_history_lines_completion(self) -> None:
|
| 930 |
+
"""
|
| 931 |
+
Start a completion based on all the other lines in the document and the
|
| 932 |
+
history.
|
| 933 |
+
"""
|
| 934 |
+
found_completions: set[str] = set()
|
| 935 |
+
completions = []
|
| 936 |
+
|
| 937 |
+
# For every line of the whole history, find matches with the current line.
|
| 938 |
+
current_line = self.document.current_line_before_cursor.lstrip()
|
| 939 |
+
|
| 940 |
+
for i, string in enumerate(self._working_lines):
|
| 941 |
+
for j, l in enumerate(string.split("\n")):
|
| 942 |
+
l = l.strip()
|
| 943 |
+
if l and l.startswith(current_line):
|
| 944 |
+
# When a new line has been found.
|
| 945 |
+
if l not in found_completions:
|
| 946 |
+
found_completions.add(l)
|
| 947 |
+
|
| 948 |
+
# Create completion.
|
| 949 |
+
if i == self.working_index:
|
| 950 |
+
display_meta = "Current, line %s" % (j + 1)
|
| 951 |
+
else:
|
| 952 |
+
display_meta = f"History {i + 1}, line {j + 1}"
|
| 953 |
+
|
| 954 |
+
completions.append(
|
| 955 |
+
Completion(
|
| 956 |
+
text=l,
|
| 957 |
+
start_position=-len(current_line),
|
| 958 |
+
display_meta=display_meta,
|
| 959 |
+
)
|
| 960 |
+
)
|
| 961 |
+
|
| 962 |
+
self._set_completions(completions=completions[::-1])
|
| 963 |
+
self.go_to_completion(0)
|
| 964 |
+
|
| 965 |
+
def go_to_completion(self, index: int | None) -> None:
|
| 966 |
+
"""
|
| 967 |
+
Select a completion from the list of current completions.
|
| 968 |
+
"""
|
| 969 |
+
assert self.complete_state
|
| 970 |
+
|
| 971 |
+
# Set new completion
|
| 972 |
+
state = self.complete_state
|
| 973 |
+
state.go_to_index(index)
|
| 974 |
+
|
| 975 |
+
# Set text/cursor position
|
| 976 |
+
new_text, new_cursor_position = state.new_text_and_position()
|
| 977 |
+
self.document = Document(new_text, new_cursor_position)
|
| 978 |
+
|
| 979 |
+
# (changing text/cursor position will unset complete_state.)
|
| 980 |
+
self.complete_state = state
|
| 981 |
+
|
| 982 |
+
def apply_completion(self, completion: Completion) -> None:
|
| 983 |
+
"""
|
| 984 |
+
Insert a given completion.
|
| 985 |
+
"""
|
| 986 |
+
# If there was already a completion active, cancel that one.
|
| 987 |
+
if self.complete_state:
|
| 988 |
+
self.go_to_completion(None)
|
| 989 |
+
self.complete_state = None
|
| 990 |
+
|
| 991 |
+
# Insert text from the given completion.
|
| 992 |
+
self.delete_before_cursor(-completion.start_position)
|
| 993 |
+
self.insert_text(completion.text)
|
| 994 |
+
|
| 995 |
+
def _set_history_search(self) -> None:
|
| 996 |
+
"""
|
| 997 |
+
Set `history_search_text`.
|
| 998 |
+
(The text before the cursor will be used for filtering the history.)
|
| 999 |
+
"""
|
| 1000 |
+
if self.enable_history_search():
|
| 1001 |
+
if self.history_search_text is None:
|
| 1002 |
+
self.history_search_text = self.document.text_before_cursor
|
| 1003 |
+
else:
|
| 1004 |
+
self.history_search_text = None
|
| 1005 |
+
|
| 1006 |
+
def _history_matches(self, i: int) -> bool:
|
| 1007 |
+
"""
|
| 1008 |
+
True when the current entry matches the history search.
|
| 1009 |
+
(when we don't have history search, it's also True.)
|
| 1010 |
+
"""
|
| 1011 |
+
return self.history_search_text is None or self._working_lines[i].startswith(
|
| 1012 |
+
self.history_search_text
|
| 1013 |
+
)
|
| 1014 |
+
|
| 1015 |
+
def history_forward(self, count: int = 1) -> None:
|
| 1016 |
+
"""
|
| 1017 |
+
Move forwards through the history.
|
| 1018 |
+
|
| 1019 |
+
:param count: Amount of items to move forward.
|
| 1020 |
+
"""
|
| 1021 |
+
self._set_history_search()
|
| 1022 |
+
|
| 1023 |
+
# Go forward in history.
|
| 1024 |
+
found_something = False
|
| 1025 |
+
|
| 1026 |
+
for i in range(self.working_index + 1, len(self._working_lines)):
|
| 1027 |
+
if self._history_matches(i):
|
| 1028 |
+
self.working_index = i
|
| 1029 |
+
count -= 1
|
| 1030 |
+
found_something = True
|
| 1031 |
+
if count == 0:
|
| 1032 |
+
break
|
| 1033 |
+
|
| 1034 |
+
# If we found an entry, move cursor to the end of the first line.
|
| 1035 |
+
if found_something:
|
| 1036 |
+
self.cursor_position = 0
|
| 1037 |
+
self.cursor_position += self.document.get_end_of_line_position()
|
| 1038 |
+
|
| 1039 |
+
def history_backward(self, count: int = 1) -> None:
|
| 1040 |
+
"""
|
| 1041 |
+
Move backwards through history.
|
| 1042 |
+
"""
|
| 1043 |
+
self._set_history_search()
|
| 1044 |
+
|
| 1045 |
+
# Go back in history.
|
| 1046 |
+
found_something = False
|
| 1047 |
+
|
| 1048 |
+
for i in range(self.working_index - 1, -1, -1):
|
| 1049 |
+
if self._history_matches(i):
|
| 1050 |
+
self.working_index = i
|
| 1051 |
+
count -= 1
|
| 1052 |
+
found_something = True
|
| 1053 |
+
if count == 0:
|
| 1054 |
+
break
|
| 1055 |
+
|
| 1056 |
+
# If we move to another entry, move cursor to the end of the line.
|
| 1057 |
+
if found_something:
|
| 1058 |
+
self.cursor_position = len(self.text)
|
| 1059 |
+
|
| 1060 |
+
def yank_nth_arg(self, n: int | None = None, _yank_last_arg: bool = False) -> None:
|
| 1061 |
+
"""
|
| 1062 |
+
Pick nth word from previous history entry (depending on current
|
| 1063 |
+
`yank_nth_arg_state`) and insert it at current position. Rotate through
|
| 1064 |
+
history if called repeatedly. If no `n` has been given, take the first
|
| 1065 |
+
argument. (The second word.)
|
| 1066 |
+
|
| 1067 |
+
:param n: (None or int), The index of the word from the previous line
|
| 1068 |
+
to take.
|
| 1069 |
+
"""
|
| 1070 |
+
assert n is None or isinstance(n, int)
|
| 1071 |
+
history_strings = self.history.get_strings()
|
| 1072 |
+
|
| 1073 |
+
if not len(history_strings):
|
| 1074 |
+
return
|
| 1075 |
+
|
| 1076 |
+
# Make sure we have a `YankNthArgState`.
|
| 1077 |
+
if self.yank_nth_arg_state is None:
|
| 1078 |
+
state = YankNthArgState(n=-1 if _yank_last_arg else 1)
|
| 1079 |
+
else:
|
| 1080 |
+
state = self.yank_nth_arg_state
|
| 1081 |
+
|
| 1082 |
+
if n is not None:
|
| 1083 |
+
state.n = n
|
| 1084 |
+
|
| 1085 |
+
# Get new history position.
|
| 1086 |
+
new_pos = state.history_position - 1
|
| 1087 |
+
if -new_pos > len(history_strings):
|
| 1088 |
+
new_pos = -1
|
| 1089 |
+
|
| 1090 |
+
# Take argument from line.
|
| 1091 |
+
line = history_strings[new_pos]
|
| 1092 |
+
|
| 1093 |
+
words = [w.strip() for w in _QUOTED_WORDS_RE.split(line)]
|
| 1094 |
+
words = [w for w in words if w]
|
| 1095 |
+
try:
|
| 1096 |
+
word = words[state.n]
|
| 1097 |
+
except IndexError:
|
| 1098 |
+
word = ""
|
| 1099 |
+
|
| 1100 |
+
# Insert new argument.
|
| 1101 |
+
if state.previous_inserted_word:
|
| 1102 |
+
self.delete_before_cursor(len(state.previous_inserted_word))
|
| 1103 |
+
self.insert_text(word)
|
| 1104 |
+
|
| 1105 |
+
# Save state again for next completion. (Note that the 'insert'
|
| 1106 |
+
# operation from above clears `self.yank_nth_arg_state`.)
|
| 1107 |
+
state.previous_inserted_word = word
|
| 1108 |
+
state.history_position = new_pos
|
| 1109 |
+
self.yank_nth_arg_state = state
|
| 1110 |
+
|
| 1111 |
+
def yank_last_arg(self, n: int | None = None) -> None:
|
| 1112 |
+
"""
|
| 1113 |
+
Like `yank_nth_arg`, but if no argument has been given, yank the last
|
| 1114 |
+
word by default.
|
| 1115 |
+
"""
|
| 1116 |
+
self.yank_nth_arg(n=n, _yank_last_arg=True)
|
| 1117 |
+
|
| 1118 |
+
def start_selection(
|
| 1119 |
+
self, selection_type: SelectionType = SelectionType.CHARACTERS
|
| 1120 |
+
) -> None:
|
| 1121 |
+
"""
|
| 1122 |
+
Take the current cursor position as the start of this selection.
|
| 1123 |
+
"""
|
| 1124 |
+
self.selection_state = SelectionState(self.cursor_position, selection_type)
|
| 1125 |
+
|
| 1126 |
+
def copy_selection(self, _cut: bool = False) -> ClipboardData:
|
| 1127 |
+
"""
|
| 1128 |
+
Copy selected text and return :class:`.ClipboardData` instance.
|
| 1129 |
+
|
| 1130 |
+
Notice that this doesn't store the copied data on the clipboard yet.
|
| 1131 |
+
You can store it like this:
|
| 1132 |
+
|
| 1133 |
+
.. code:: python
|
| 1134 |
+
|
| 1135 |
+
data = buffer.copy_selection()
|
| 1136 |
+
get_app().clipboard.set_data(data)
|
| 1137 |
+
"""
|
| 1138 |
+
new_document, clipboard_data = self.document.cut_selection()
|
| 1139 |
+
if _cut:
|
| 1140 |
+
self.document = new_document
|
| 1141 |
+
|
| 1142 |
+
self.selection_state = None
|
| 1143 |
+
return clipboard_data
|
| 1144 |
+
|
| 1145 |
+
def cut_selection(self) -> ClipboardData:
|
| 1146 |
+
"""
|
| 1147 |
+
Delete selected text and return :class:`.ClipboardData` instance.
|
| 1148 |
+
"""
|
| 1149 |
+
return self.copy_selection(_cut=True)
|
| 1150 |
+
|
| 1151 |
+
def paste_clipboard_data(
|
| 1152 |
+
self,
|
| 1153 |
+
data: ClipboardData,
|
| 1154 |
+
paste_mode: PasteMode = PasteMode.EMACS,
|
| 1155 |
+
count: int = 1,
|
| 1156 |
+
) -> None:
|
| 1157 |
+
"""
|
| 1158 |
+
Insert the data from the clipboard.
|
| 1159 |
+
"""
|
| 1160 |
+
assert isinstance(data, ClipboardData)
|
| 1161 |
+
assert paste_mode in (PasteMode.VI_BEFORE, PasteMode.VI_AFTER, PasteMode.EMACS)
|
| 1162 |
+
|
| 1163 |
+
original_document = self.document
|
| 1164 |
+
self.document = self.document.paste_clipboard_data(
|
| 1165 |
+
data, paste_mode=paste_mode, count=count
|
| 1166 |
+
)
|
| 1167 |
+
|
| 1168 |
+
# Remember original document. This assignment should come at the end,
|
| 1169 |
+
# because assigning to 'document' will erase it.
|
| 1170 |
+
self.document_before_paste = original_document
|
| 1171 |
+
|
| 1172 |
+
def newline(self, copy_margin: bool = True) -> None:
|
| 1173 |
+
"""
|
| 1174 |
+
Insert a line ending at the current position.
|
| 1175 |
+
"""
|
| 1176 |
+
if copy_margin:
|
| 1177 |
+
self.insert_text("\n" + self.document.leading_whitespace_in_current_line)
|
| 1178 |
+
else:
|
| 1179 |
+
self.insert_text("\n")
|
| 1180 |
+
|
| 1181 |
+
def insert_line_above(self, copy_margin: bool = True) -> None:
|
| 1182 |
+
"""
|
| 1183 |
+
Insert a new line above the current one.
|
| 1184 |
+
"""
|
| 1185 |
+
if copy_margin:
|
| 1186 |
+
insert = self.document.leading_whitespace_in_current_line + "\n"
|
| 1187 |
+
else:
|
| 1188 |
+
insert = "\n"
|
| 1189 |
+
|
| 1190 |
+
self.cursor_position += self.document.get_start_of_line_position()
|
| 1191 |
+
self.insert_text(insert)
|
| 1192 |
+
self.cursor_position -= 1
|
| 1193 |
+
|
| 1194 |
+
def insert_line_below(self, copy_margin: bool = True) -> None:
|
| 1195 |
+
"""
|
| 1196 |
+
Insert a new line below the current one.
|
| 1197 |
+
"""
|
| 1198 |
+
if copy_margin:
|
| 1199 |
+
insert = "\n" + self.document.leading_whitespace_in_current_line
|
| 1200 |
+
else:
|
| 1201 |
+
insert = "\n"
|
| 1202 |
+
|
| 1203 |
+
self.cursor_position += self.document.get_end_of_line_position()
|
| 1204 |
+
self.insert_text(insert)
|
| 1205 |
+
|
| 1206 |
+
def insert_text(
|
| 1207 |
+
self,
|
| 1208 |
+
data: str,
|
| 1209 |
+
overwrite: bool = False,
|
| 1210 |
+
move_cursor: bool = True,
|
| 1211 |
+
fire_event: bool = True,
|
| 1212 |
+
) -> None:
|
| 1213 |
+
"""
|
| 1214 |
+
Insert characters at cursor position.
|
| 1215 |
+
|
| 1216 |
+
:param fire_event: Fire `on_text_insert` event. This is mainly used to
|
| 1217 |
+
trigger autocompletion while typing.
|
| 1218 |
+
"""
|
| 1219 |
+
# Original text & cursor position.
|
| 1220 |
+
otext = self.text
|
| 1221 |
+
ocpos = self.cursor_position
|
| 1222 |
+
|
| 1223 |
+
# In insert/text mode.
|
| 1224 |
+
if overwrite:
|
| 1225 |
+
# Don't overwrite the newline itself. Just before the line ending,
|
| 1226 |
+
# it should act like insert mode.
|
| 1227 |
+
overwritten_text = otext[ocpos : ocpos + len(data)]
|
| 1228 |
+
if "\n" in overwritten_text:
|
| 1229 |
+
overwritten_text = overwritten_text[: overwritten_text.find("\n")]
|
| 1230 |
+
|
| 1231 |
+
text = otext[:ocpos] + data + otext[ocpos + len(overwritten_text) :]
|
| 1232 |
+
else:
|
| 1233 |
+
text = otext[:ocpos] + data + otext[ocpos:]
|
| 1234 |
+
|
| 1235 |
+
if move_cursor:
|
| 1236 |
+
cpos = self.cursor_position + len(data)
|
| 1237 |
+
else:
|
| 1238 |
+
cpos = self.cursor_position
|
| 1239 |
+
|
| 1240 |
+
# Set new document.
|
| 1241 |
+
# (Set text and cursor position at the same time. Otherwise, setting
|
| 1242 |
+
# the text will fire a change event before the cursor position has been
|
| 1243 |
+
# set. It works better to have this atomic.)
|
| 1244 |
+
self.document = Document(text, cpos)
|
| 1245 |
+
|
| 1246 |
+
# Fire 'on_text_insert' event.
|
| 1247 |
+
if fire_event: # XXX: rename to `start_complete`.
|
| 1248 |
+
self.on_text_insert.fire()
|
| 1249 |
+
|
| 1250 |
+
# Only complete when "complete_while_typing" is enabled.
|
| 1251 |
+
if self.completer and self.complete_while_typing():
|
| 1252 |
+
get_app().create_background_task(self._async_completer())
|
| 1253 |
+
|
| 1254 |
+
# Call auto_suggest.
|
| 1255 |
+
if self.auto_suggest:
|
| 1256 |
+
get_app().create_background_task(self._async_suggester())
|
| 1257 |
+
|
| 1258 |
+
def undo(self) -> None:
|
| 1259 |
+
# Pop from the undo-stack until we find a text that if different from
|
| 1260 |
+
# the current text. (The current logic of `save_to_undo_stack` will
|
| 1261 |
+
# cause that the top of the undo stack is usually the same as the
|
| 1262 |
+
# current text, so in that case we have to pop twice.)
|
| 1263 |
+
while self._undo_stack:
|
| 1264 |
+
text, pos = self._undo_stack.pop()
|
| 1265 |
+
|
| 1266 |
+
if text != self.text:
|
| 1267 |
+
# Push current text to redo stack.
|
| 1268 |
+
self._redo_stack.append((self.text, self.cursor_position))
|
| 1269 |
+
|
| 1270 |
+
# Set new text/cursor_position.
|
| 1271 |
+
self.document = Document(text, cursor_position=pos)
|
| 1272 |
+
break
|
| 1273 |
+
|
| 1274 |
+
def redo(self) -> None:
|
| 1275 |
+
if self._redo_stack:
|
| 1276 |
+
# Copy current state on undo stack.
|
| 1277 |
+
self.save_to_undo_stack(clear_redo_stack=False)
|
| 1278 |
+
|
| 1279 |
+
# Pop state from redo stack.
|
| 1280 |
+
text, pos = self._redo_stack.pop()
|
| 1281 |
+
self.document = Document(text, cursor_position=pos)
|
| 1282 |
+
|
| 1283 |
+
def validate(self, set_cursor: bool = False) -> bool:
|
| 1284 |
+
"""
|
| 1285 |
+
Returns `True` if valid.
|
| 1286 |
+
|
| 1287 |
+
:param set_cursor: Set the cursor position, if an error was found.
|
| 1288 |
+
"""
|
| 1289 |
+
# Don't call the validator again, if it was already called for the
|
| 1290 |
+
# current input.
|
| 1291 |
+
if self.validation_state != ValidationState.UNKNOWN:
|
| 1292 |
+
return self.validation_state == ValidationState.VALID
|
| 1293 |
+
|
| 1294 |
+
# Call validator.
|
| 1295 |
+
if self.validator:
|
| 1296 |
+
try:
|
| 1297 |
+
self.validator.validate(self.document)
|
| 1298 |
+
except ValidationError as e:
|
| 1299 |
+
# Set cursor position (don't allow invalid values.)
|
| 1300 |
+
if set_cursor:
|
| 1301 |
+
self.cursor_position = min(
|
| 1302 |
+
max(0, e.cursor_position), len(self.text)
|
| 1303 |
+
)
|
| 1304 |
+
|
| 1305 |
+
self.validation_state = ValidationState.INVALID
|
| 1306 |
+
self.validation_error = e
|
| 1307 |
+
return False
|
| 1308 |
+
|
| 1309 |
+
# Handle validation result.
|
| 1310 |
+
self.validation_state = ValidationState.VALID
|
| 1311 |
+
self.validation_error = None
|
| 1312 |
+
return True
|
| 1313 |
+
|
| 1314 |
+
async def _validate_async(self) -> None:
|
| 1315 |
+
"""
|
| 1316 |
+
Asynchronous version of `validate()`.
|
| 1317 |
+
This one doesn't set the cursor position.
|
| 1318 |
+
|
| 1319 |
+
We have both variants, because a synchronous version is required.
|
| 1320 |
+
Handling the ENTER key needs to be completely synchronous, otherwise
|
| 1321 |
+
stuff like type-ahead is going to give very weird results. (People
|
| 1322 |
+
could type input while the ENTER key is still processed.)
|
| 1323 |
+
|
| 1324 |
+
An asynchronous version is required if we have `validate_while_typing`
|
| 1325 |
+
enabled.
|
| 1326 |
+
"""
|
| 1327 |
+
while True:
|
| 1328 |
+
# Don't call the validator again, if it was already called for the
|
| 1329 |
+
# current input.
|
| 1330 |
+
if self.validation_state != ValidationState.UNKNOWN:
|
| 1331 |
+
return
|
| 1332 |
+
|
| 1333 |
+
# Call validator.
|
| 1334 |
+
error = None
|
| 1335 |
+
document = self.document
|
| 1336 |
+
|
| 1337 |
+
if self.validator:
|
| 1338 |
+
try:
|
| 1339 |
+
await self.validator.validate_async(self.document)
|
| 1340 |
+
except ValidationError as e:
|
| 1341 |
+
error = e
|
| 1342 |
+
|
| 1343 |
+
# If the document changed during the validation, try again.
|
| 1344 |
+
if self.document != document:
|
| 1345 |
+
continue
|
| 1346 |
+
|
| 1347 |
+
# Handle validation result.
|
| 1348 |
+
if error:
|
| 1349 |
+
self.validation_state = ValidationState.INVALID
|
| 1350 |
+
else:
|
| 1351 |
+
self.validation_state = ValidationState.VALID
|
| 1352 |
+
|
| 1353 |
+
self.validation_error = error
|
| 1354 |
+
get_app().invalidate() # Trigger redraw (display error).
|
| 1355 |
+
|
| 1356 |
+
def append_to_history(self) -> None:
|
| 1357 |
+
"""
|
| 1358 |
+
Append the current input to the history.
|
| 1359 |
+
"""
|
| 1360 |
+
# Save at the tail of the history. (But don't if the last entry the
|
| 1361 |
+
# history is already the same.)
|
| 1362 |
+
if self.text:
|
| 1363 |
+
history_strings = self.history.get_strings()
|
| 1364 |
+
if not len(history_strings) or history_strings[-1] != self.text:
|
| 1365 |
+
self.history.append_string(self.text)
|
| 1366 |
+
|
| 1367 |
+
def _search(
|
| 1368 |
+
self,
|
| 1369 |
+
search_state: SearchState,
|
| 1370 |
+
include_current_position: bool = False,
|
| 1371 |
+
count: int = 1,
|
| 1372 |
+
) -> tuple[int, int] | None:
|
| 1373 |
+
"""
|
| 1374 |
+
Execute search. Return (working_index, cursor_position) tuple when this
|
| 1375 |
+
search is applied. Returns `None` when this text cannot be found.
|
| 1376 |
+
"""
|
| 1377 |
+
assert count > 0
|
| 1378 |
+
|
| 1379 |
+
text = search_state.text
|
| 1380 |
+
direction = search_state.direction
|
| 1381 |
+
ignore_case = search_state.ignore_case()
|
| 1382 |
+
|
| 1383 |
+
def search_once(
|
| 1384 |
+
working_index: int, document: Document
|
| 1385 |
+
) -> tuple[int, Document] | None:
|
| 1386 |
+
"""
|
| 1387 |
+
Do search one time.
|
| 1388 |
+
Return (working_index, document) or `None`
|
| 1389 |
+
"""
|
| 1390 |
+
if direction == SearchDirection.FORWARD:
|
| 1391 |
+
# Try find at the current input.
|
| 1392 |
+
new_index = document.find(
|
| 1393 |
+
text,
|
| 1394 |
+
include_current_position=include_current_position,
|
| 1395 |
+
ignore_case=ignore_case,
|
| 1396 |
+
)
|
| 1397 |
+
|
| 1398 |
+
if new_index is not None:
|
| 1399 |
+
return (
|
| 1400 |
+
working_index,
|
| 1401 |
+
Document(document.text, document.cursor_position + new_index),
|
| 1402 |
+
)
|
| 1403 |
+
else:
|
| 1404 |
+
# No match, go forward in the history. (Include len+1 to wrap around.)
|
| 1405 |
+
# (Here we should always include all cursor positions, because
|
| 1406 |
+
# it's a different line.)
|
| 1407 |
+
for i in range(working_index + 1, len(self._working_lines) + 1):
|
| 1408 |
+
i %= len(self._working_lines)
|
| 1409 |
+
|
| 1410 |
+
document = Document(self._working_lines[i], 0)
|
| 1411 |
+
new_index = document.find(
|
| 1412 |
+
text, include_current_position=True, ignore_case=ignore_case
|
| 1413 |
+
)
|
| 1414 |
+
if new_index is not None:
|
| 1415 |
+
return (i, Document(document.text, new_index))
|
| 1416 |
+
else:
|
| 1417 |
+
# Try find at the current input.
|
| 1418 |
+
new_index = document.find_backwards(text, ignore_case=ignore_case)
|
| 1419 |
+
|
| 1420 |
+
if new_index is not None:
|
| 1421 |
+
return (
|
| 1422 |
+
working_index,
|
| 1423 |
+
Document(document.text, document.cursor_position + new_index),
|
| 1424 |
+
)
|
| 1425 |
+
else:
|
| 1426 |
+
# No match, go back in the history. (Include -1 to wrap around.)
|
| 1427 |
+
for i in range(working_index - 1, -2, -1):
|
| 1428 |
+
i %= len(self._working_lines)
|
| 1429 |
+
|
| 1430 |
+
document = Document(
|
| 1431 |
+
self._working_lines[i], len(self._working_lines[i])
|
| 1432 |
+
)
|
| 1433 |
+
new_index = document.find_backwards(
|
| 1434 |
+
text, ignore_case=ignore_case
|
| 1435 |
+
)
|
| 1436 |
+
if new_index is not None:
|
| 1437 |
+
return (
|
| 1438 |
+
i,
|
| 1439 |
+
Document(document.text, len(document.text) + new_index),
|
| 1440 |
+
)
|
| 1441 |
+
return None
|
| 1442 |
+
|
| 1443 |
+
# Do 'count' search iterations.
|
| 1444 |
+
working_index = self.working_index
|
| 1445 |
+
document = self.document
|
| 1446 |
+
for _ in range(count):
|
| 1447 |
+
result = search_once(working_index, document)
|
| 1448 |
+
if result is None:
|
| 1449 |
+
return None # Nothing found.
|
| 1450 |
+
else:
|
| 1451 |
+
working_index, document = result
|
| 1452 |
+
|
| 1453 |
+
return (working_index, document.cursor_position)
|
| 1454 |
+
|
| 1455 |
+
def document_for_search(self, search_state: SearchState) -> Document:
|
| 1456 |
+
"""
|
| 1457 |
+
Return a :class:`~prompt_toolkit.document.Document` instance that has
|
| 1458 |
+
the text/cursor position for this search, if we would apply it. This
|
| 1459 |
+
will be used in the
|
| 1460 |
+
:class:`~prompt_toolkit.layout.BufferControl` to display feedback while
|
| 1461 |
+
searching.
|
| 1462 |
+
"""
|
| 1463 |
+
search_result = self._search(search_state, include_current_position=True)
|
| 1464 |
+
|
| 1465 |
+
if search_result is None:
|
| 1466 |
+
return self.document
|
| 1467 |
+
else:
|
| 1468 |
+
working_index, cursor_position = search_result
|
| 1469 |
+
|
| 1470 |
+
# Keep selection, when `working_index` was not changed.
|
| 1471 |
+
if working_index == self.working_index:
|
| 1472 |
+
selection = self.selection_state
|
| 1473 |
+
else:
|
| 1474 |
+
selection = None
|
| 1475 |
+
|
| 1476 |
+
return Document(
|
| 1477 |
+
self._working_lines[working_index], cursor_position, selection=selection
|
| 1478 |
+
)
|
| 1479 |
+
|
| 1480 |
+
def get_search_position(
|
| 1481 |
+
self,
|
| 1482 |
+
search_state: SearchState,
|
| 1483 |
+
include_current_position: bool = True,
|
| 1484 |
+
count: int = 1,
|
| 1485 |
+
) -> int:
|
| 1486 |
+
"""
|
| 1487 |
+
Get the cursor position for this search.
|
| 1488 |
+
(This operation won't change the `working_index`. It's won't go through
|
| 1489 |
+
the history. Vi text objects can't span multiple items.)
|
| 1490 |
+
"""
|
| 1491 |
+
search_result = self._search(
|
| 1492 |
+
search_state, include_current_position=include_current_position, count=count
|
| 1493 |
+
)
|
| 1494 |
+
|
| 1495 |
+
if search_result is None:
|
| 1496 |
+
return self.cursor_position
|
| 1497 |
+
else:
|
| 1498 |
+
working_index, cursor_position = search_result
|
| 1499 |
+
return cursor_position
|
| 1500 |
+
|
| 1501 |
+
def apply_search(
|
| 1502 |
+
self,
|
| 1503 |
+
search_state: SearchState,
|
| 1504 |
+
include_current_position: bool = True,
|
| 1505 |
+
count: int = 1,
|
| 1506 |
+
) -> None:
|
| 1507 |
+
"""
|
| 1508 |
+
Apply search. If something is found, set `working_index` and
|
| 1509 |
+
`cursor_position`.
|
| 1510 |
+
"""
|
| 1511 |
+
search_result = self._search(
|
| 1512 |
+
search_state, include_current_position=include_current_position, count=count
|
| 1513 |
+
)
|
| 1514 |
+
|
| 1515 |
+
if search_result is not None:
|
| 1516 |
+
working_index, cursor_position = search_result
|
| 1517 |
+
self.working_index = working_index
|
| 1518 |
+
self.cursor_position = cursor_position
|
| 1519 |
+
|
| 1520 |
+
def exit_selection(self) -> None:
|
| 1521 |
+
self.selection_state = None
|
| 1522 |
+
|
| 1523 |
+
def _editor_simple_tempfile(self) -> tuple[str, Callable[[], None]]:
|
| 1524 |
+
"""
|
| 1525 |
+
Simple (file) tempfile implementation.
|
| 1526 |
+
Return (tempfile, cleanup_func).
|
| 1527 |
+
"""
|
| 1528 |
+
suffix = to_str(self.tempfile_suffix)
|
| 1529 |
+
descriptor, filename = tempfile.mkstemp(suffix)
|
| 1530 |
+
|
| 1531 |
+
os.write(descriptor, self.text.encode("utf-8"))
|
| 1532 |
+
os.close(descriptor)
|
| 1533 |
+
|
| 1534 |
+
def cleanup() -> None:
|
| 1535 |
+
os.unlink(filename)
|
| 1536 |
+
|
| 1537 |
+
return filename, cleanup
|
| 1538 |
+
|
| 1539 |
+
def _editor_complex_tempfile(self) -> tuple[str, Callable[[], None]]:
|
| 1540 |
+
# Complex (directory) tempfile implementation.
|
| 1541 |
+
headtail = to_str(self.tempfile)
|
| 1542 |
+
if not headtail:
|
| 1543 |
+
# Revert to simple case.
|
| 1544 |
+
return self._editor_simple_tempfile()
|
| 1545 |
+
headtail = str(headtail)
|
| 1546 |
+
|
| 1547 |
+
# Try to make according to tempfile logic.
|
| 1548 |
+
head, tail = os.path.split(headtail)
|
| 1549 |
+
if os.path.isabs(head):
|
| 1550 |
+
head = head[1:]
|
| 1551 |
+
|
| 1552 |
+
dirpath = tempfile.mkdtemp()
|
| 1553 |
+
if head:
|
| 1554 |
+
dirpath = os.path.join(dirpath, head)
|
| 1555 |
+
# Assume there is no issue creating dirs in this temp dir.
|
| 1556 |
+
os.makedirs(dirpath)
|
| 1557 |
+
|
| 1558 |
+
# Open the filename and write current text.
|
| 1559 |
+
filename = os.path.join(dirpath, tail)
|
| 1560 |
+
with open(filename, "w", encoding="utf-8") as fh:
|
| 1561 |
+
fh.write(self.text)
|
| 1562 |
+
|
| 1563 |
+
def cleanup() -> None:
|
| 1564 |
+
shutil.rmtree(dirpath)
|
| 1565 |
+
|
| 1566 |
+
return filename, cleanup
|
| 1567 |
+
|
| 1568 |
+
def open_in_editor(self, validate_and_handle: bool = False) -> asyncio.Task[None]:
|
| 1569 |
+
"""
|
| 1570 |
+
Open code in editor.
|
| 1571 |
+
|
| 1572 |
+
This returns a future, and runs in a thread executor.
|
| 1573 |
+
"""
|
| 1574 |
+
if self.read_only():
|
| 1575 |
+
raise EditReadOnlyBuffer()
|
| 1576 |
+
|
| 1577 |
+
# Write current text to temporary file
|
| 1578 |
+
if self.tempfile:
|
| 1579 |
+
filename, cleanup_func = self._editor_complex_tempfile()
|
| 1580 |
+
else:
|
| 1581 |
+
filename, cleanup_func = self._editor_simple_tempfile()
|
| 1582 |
+
|
| 1583 |
+
async def run() -> None:
|
| 1584 |
+
try:
|
| 1585 |
+
# Open in editor
|
| 1586 |
+
# (We need to use `run_in_terminal`, because not all editors go to
|
| 1587 |
+
# the alternate screen buffer, and some could influence the cursor
|
| 1588 |
+
# position.)
|
| 1589 |
+
success = await run_in_terminal(
|
| 1590 |
+
lambda: self._open_file_in_editor(filename), in_executor=True
|
| 1591 |
+
)
|
| 1592 |
+
|
| 1593 |
+
# Read content again.
|
| 1594 |
+
if success:
|
| 1595 |
+
with open(filename, "rb") as f:
|
| 1596 |
+
text = f.read().decode("utf-8")
|
| 1597 |
+
|
| 1598 |
+
# Drop trailing newline. (Editors are supposed to add it at the
|
| 1599 |
+
# end, but we don't need it.)
|
| 1600 |
+
if text.endswith("\n"):
|
| 1601 |
+
text = text[:-1]
|
| 1602 |
+
|
| 1603 |
+
self.document = Document(text=text, cursor_position=len(text))
|
| 1604 |
+
|
| 1605 |
+
# Accept the input.
|
| 1606 |
+
if validate_and_handle:
|
| 1607 |
+
self.validate_and_handle()
|
| 1608 |
+
|
| 1609 |
+
finally:
|
| 1610 |
+
# Clean up temp dir/file.
|
| 1611 |
+
cleanup_func()
|
| 1612 |
+
|
| 1613 |
+
return get_app().create_background_task(run())
|
| 1614 |
+
|
| 1615 |
+
def _open_file_in_editor(self, filename: str) -> bool:
|
| 1616 |
+
"""
|
| 1617 |
+
Call editor executable.
|
| 1618 |
+
|
| 1619 |
+
Return True when we received a zero return code.
|
| 1620 |
+
"""
|
| 1621 |
+
# If the 'VISUAL' or 'EDITOR' environment variable has been set, use that.
|
| 1622 |
+
# Otherwise, fall back to the first available editor that we can find.
|
| 1623 |
+
visual = os.environ.get("VISUAL")
|
| 1624 |
+
editor = os.environ.get("EDITOR")
|
| 1625 |
+
|
| 1626 |
+
editors = [
|
| 1627 |
+
visual,
|
| 1628 |
+
editor,
|
| 1629 |
+
# Order of preference.
|
| 1630 |
+
"/usr/bin/editor",
|
| 1631 |
+
"/usr/bin/nano",
|
| 1632 |
+
"/usr/bin/pico",
|
| 1633 |
+
"/usr/bin/vi",
|
| 1634 |
+
"/usr/bin/emacs",
|
| 1635 |
+
]
|
| 1636 |
+
|
| 1637 |
+
for e in editors:
|
| 1638 |
+
if e:
|
| 1639 |
+
try:
|
| 1640 |
+
# Use 'shlex.split()', because $VISUAL can contain spaces
|
| 1641 |
+
# and quotes.
|
| 1642 |
+
returncode = subprocess.call(shlex.split(e) + [filename])
|
| 1643 |
+
return returncode == 0
|
| 1644 |
+
|
| 1645 |
+
except OSError:
|
| 1646 |
+
# Executable does not exist, try the next one.
|
| 1647 |
+
pass
|
| 1648 |
+
|
| 1649 |
+
return False
|
| 1650 |
+
|
| 1651 |
+
def start_completion(
|
| 1652 |
+
self,
|
| 1653 |
+
select_first: bool = False,
|
| 1654 |
+
select_last: bool = False,
|
| 1655 |
+
insert_common_part: bool = False,
|
| 1656 |
+
complete_event: CompleteEvent | None = None,
|
| 1657 |
+
) -> None:
|
| 1658 |
+
"""
|
| 1659 |
+
Start asynchronous autocompletion of this buffer.
|
| 1660 |
+
(This will do nothing if a previous completion was still in progress.)
|
| 1661 |
+
"""
|
| 1662 |
+
# Only one of these options can be selected.
|
| 1663 |
+
assert select_first + select_last + insert_common_part <= 1
|
| 1664 |
+
|
| 1665 |
+
get_app().create_background_task(
|
| 1666 |
+
self._async_completer(
|
| 1667 |
+
select_first=select_first,
|
| 1668 |
+
select_last=select_last,
|
| 1669 |
+
insert_common_part=insert_common_part,
|
| 1670 |
+
complete_event=complete_event
|
| 1671 |
+
or CompleteEvent(completion_requested=True),
|
| 1672 |
+
)
|
| 1673 |
+
)
|
| 1674 |
+
|
| 1675 |
+
def _create_completer_coroutine(self) -> Callable[..., Coroutine[Any, Any, None]]:
|
| 1676 |
+
"""
|
| 1677 |
+
Create function for asynchronous autocompletion.
|
| 1678 |
+
|
| 1679 |
+
(This consumes the asynchronous completer generator, which possibly
|
| 1680 |
+
runs the completion algorithm in another thread.)
|
| 1681 |
+
"""
|
| 1682 |
+
|
| 1683 |
+
def completion_does_nothing(document: Document, completion: Completion) -> bool:
|
| 1684 |
+
"""
|
| 1685 |
+
Return `True` if applying this completion doesn't have any effect.
|
| 1686 |
+
(When it doesn't insert any new text.
|
| 1687 |
+
"""
|
| 1688 |
+
text_before_cursor = document.text_before_cursor
|
| 1689 |
+
replaced_text = text_before_cursor[
|
| 1690 |
+
len(text_before_cursor) + completion.start_position :
|
| 1691 |
+
]
|
| 1692 |
+
return replaced_text == completion.text
|
| 1693 |
+
|
| 1694 |
+
@_only_one_at_a_time
|
| 1695 |
+
async def async_completer(
|
| 1696 |
+
select_first: bool = False,
|
| 1697 |
+
select_last: bool = False,
|
| 1698 |
+
insert_common_part: bool = False,
|
| 1699 |
+
complete_event: CompleteEvent | None = None,
|
| 1700 |
+
) -> None:
|
| 1701 |
+
document = self.document
|
| 1702 |
+
complete_event = complete_event or CompleteEvent(text_inserted=True)
|
| 1703 |
+
|
| 1704 |
+
# Don't complete when we already have completions.
|
| 1705 |
+
if self.complete_state or not self.completer:
|
| 1706 |
+
return
|
| 1707 |
+
|
| 1708 |
+
# Create an empty CompletionState.
|
| 1709 |
+
complete_state = CompletionState(original_document=self.document)
|
| 1710 |
+
self.complete_state = complete_state
|
| 1711 |
+
|
| 1712 |
+
def proceed() -> bool:
|
| 1713 |
+
"""Keep retrieving completions. Input text has not yet changed
|
| 1714 |
+
while generating completions."""
|
| 1715 |
+
return self.complete_state == complete_state
|
| 1716 |
+
|
| 1717 |
+
refresh_needed = asyncio.Event()
|
| 1718 |
+
|
| 1719 |
+
async def refresh_while_loading() -> None:
|
| 1720 |
+
"""Background loop to refresh the UI at most 3 times a second
|
| 1721 |
+
while the completion are loading. Calling
|
| 1722 |
+
`on_completions_changed.fire()` for every completion that we
|
| 1723 |
+
receive is too expensive when there are many completions. (We
|
| 1724 |
+
could tune `Application.max_render_postpone_time` and
|
| 1725 |
+
`Application.min_redraw_interval`, but having this here is a
|
| 1726 |
+
better approach.)
|
| 1727 |
+
"""
|
| 1728 |
+
while True:
|
| 1729 |
+
self.on_completions_changed.fire()
|
| 1730 |
+
refresh_needed.clear()
|
| 1731 |
+
await asyncio.sleep(0.3)
|
| 1732 |
+
await refresh_needed.wait()
|
| 1733 |
+
|
| 1734 |
+
refresh_task = asyncio.ensure_future(refresh_while_loading())
|
| 1735 |
+
try:
|
| 1736 |
+
# Load.
|
| 1737 |
+
async with aclosing(
|
| 1738 |
+
self.completer.get_completions_async(document, complete_event)
|
| 1739 |
+
) as async_generator:
|
| 1740 |
+
async for completion in async_generator:
|
| 1741 |
+
complete_state.completions.append(completion)
|
| 1742 |
+
refresh_needed.set()
|
| 1743 |
+
|
| 1744 |
+
# If the input text changes, abort.
|
| 1745 |
+
if not proceed():
|
| 1746 |
+
break
|
| 1747 |
+
|
| 1748 |
+
# Always stop at 10k completions.
|
| 1749 |
+
if (
|
| 1750 |
+
len(complete_state.completions)
|
| 1751 |
+
>= self.max_number_of_completions
|
| 1752 |
+
):
|
| 1753 |
+
break
|
| 1754 |
+
finally:
|
| 1755 |
+
refresh_task.cancel()
|
| 1756 |
+
|
| 1757 |
+
# Refresh one final time after we got everything.
|
| 1758 |
+
self.on_completions_changed.fire()
|
| 1759 |
+
|
| 1760 |
+
completions = complete_state.completions
|
| 1761 |
+
|
| 1762 |
+
# When there is only one completion, which has nothing to add, ignore it.
|
| 1763 |
+
if len(completions) == 1 and completion_does_nothing(
|
| 1764 |
+
document, completions[0]
|
| 1765 |
+
):
|
| 1766 |
+
del completions[:]
|
| 1767 |
+
|
| 1768 |
+
# Set completions if the text was not yet changed.
|
| 1769 |
+
if proceed():
|
| 1770 |
+
# When no completions were found, or when the user selected
|
| 1771 |
+
# already a completion by using the arrow keys, don't do anything.
|
| 1772 |
+
if (
|
| 1773 |
+
not self.complete_state
|
| 1774 |
+
or self.complete_state.complete_index is not None
|
| 1775 |
+
):
|
| 1776 |
+
return
|
| 1777 |
+
|
| 1778 |
+
# When there are no completions, reset completion state anyway.
|
| 1779 |
+
if not completions:
|
| 1780 |
+
self.complete_state = None
|
| 1781 |
+
# Render the ui if the completion menu was shown
|
| 1782 |
+
# it is needed especially if there is one completion and it was deleted.
|
| 1783 |
+
self.on_completions_changed.fire()
|
| 1784 |
+
return
|
| 1785 |
+
|
| 1786 |
+
# Select first/last or insert common part, depending on the key
|
| 1787 |
+
# binding. (For this we have to wait until all completions are
|
| 1788 |
+
# loaded.)
|
| 1789 |
+
|
| 1790 |
+
if select_first:
|
| 1791 |
+
self.go_to_completion(0)
|
| 1792 |
+
|
| 1793 |
+
elif select_last:
|
| 1794 |
+
self.go_to_completion(len(completions) - 1)
|
| 1795 |
+
|
| 1796 |
+
elif insert_common_part:
|
| 1797 |
+
common_part = get_common_complete_suffix(document, completions)
|
| 1798 |
+
if common_part:
|
| 1799 |
+
# Insert the common part, update completions.
|
| 1800 |
+
self.insert_text(common_part)
|
| 1801 |
+
if len(completions) > 1:
|
| 1802 |
+
# (Don't call `async_completer` again, but
|
| 1803 |
+
# recalculate completions. See:
|
| 1804 |
+
# https://github.com/ipython/ipython/issues/9658)
|
| 1805 |
+
completions[:] = [
|
| 1806 |
+
c.new_completion_from_position(len(common_part))
|
| 1807 |
+
for c in completions
|
| 1808 |
+
]
|
| 1809 |
+
|
| 1810 |
+
self._set_completions(completions=completions)
|
| 1811 |
+
else:
|
| 1812 |
+
self.complete_state = None
|
| 1813 |
+
else:
|
| 1814 |
+
# When we were asked to insert the "common"
|
| 1815 |
+
# prefix, but there was no common suffix but
|
| 1816 |
+
# still exactly one match, then select the
|
| 1817 |
+
# first. (It could be that we have a completion
|
| 1818 |
+
# which does * expansion, like '*.py', with
|
| 1819 |
+
# exactly one match.)
|
| 1820 |
+
if len(completions) == 1:
|
| 1821 |
+
self.go_to_completion(0)
|
| 1822 |
+
|
| 1823 |
+
else:
|
| 1824 |
+
# If the last operation was an insert, (not a delete), restart
|
| 1825 |
+
# the completion coroutine.
|
| 1826 |
+
|
| 1827 |
+
if self.document.text_before_cursor == document.text_before_cursor:
|
| 1828 |
+
return # Nothing changed.
|
| 1829 |
+
|
| 1830 |
+
if self.document.text_before_cursor.startswith(
|
| 1831 |
+
document.text_before_cursor
|
| 1832 |
+
):
|
| 1833 |
+
raise _Retry
|
| 1834 |
+
|
| 1835 |
+
return async_completer
|
| 1836 |
+
|
| 1837 |
+
def _create_auto_suggest_coroutine(self) -> Callable[[], Coroutine[Any, Any, None]]:
|
| 1838 |
+
"""
|
| 1839 |
+
Create function for asynchronous auto suggestion.
|
| 1840 |
+
(This can be in another thread.)
|
| 1841 |
+
"""
|
| 1842 |
+
|
| 1843 |
+
@_only_one_at_a_time
|
| 1844 |
+
async def async_suggestor() -> None:
|
| 1845 |
+
document = self.document
|
| 1846 |
+
|
| 1847 |
+
# Don't suggest when we already have a suggestion.
|
| 1848 |
+
if self.suggestion or not self.auto_suggest:
|
| 1849 |
+
return
|
| 1850 |
+
|
| 1851 |
+
suggestion = await self.auto_suggest.get_suggestion_async(self, document)
|
| 1852 |
+
|
| 1853 |
+
# Set suggestion only if the text was not yet changed.
|
| 1854 |
+
if self.document == document:
|
| 1855 |
+
# Set suggestion and redraw interface.
|
| 1856 |
+
self.suggestion = suggestion
|
| 1857 |
+
self.on_suggestion_set.fire()
|
| 1858 |
+
else:
|
| 1859 |
+
# Otherwise, restart thread.
|
| 1860 |
+
raise _Retry
|
| 1861 |
+
|
| 1862 |
+
return async_suggestor
|
| 1863 |
+
|
| 1864 |
+
def _create_auto_validate_coroutine(
|
| 1865 |
+
self,
|
| 1866 |
+
) -> Callable[[], Coroutine[Any, Any, None]]:
|
| 1867 |
+
"""
|
| 1868 |
+
Create a function for asynchronous validation while typing.
|
| 1869 |
+
(This can be in another thread.)
|
| 1870 |
+
"""
|
| 1871 |
+
|
| 1872 |
+
@_only_one_at_a_time
|
| 1873 |
+
async def async_validator() -> None:
|
| 1874 |
+
await self._validate_async()
|
| 1875 |
+
|
| 1876 |
+
return async_validator
|
| 1877 |
+
|
| 1878 |
+
def validate_and_handle(self) -> None:
|
| 1879 |
+
"""
|
| 1880 |
+
Validate buffer and handle the accept action.
|
| 1881 |
+
"""
|
| 1882 |
+
valid = self.validate(set_cursor=True)
|
| 1883 |
+
|
| 1884 |
+
# When the validation succeeded, accept the input.
|
| 1885 |
+
if valid:
|
| 1886 |
+
if self.accept_handler:
|
| 1887 |
+
keep_text = self.accept_handler(self)
|
| 1888 |
+
else:
|
| 1889 |
+
keep_text = False
|
| 1890 |
+
|
| 1891 |
+
self.append_to_history()
|
| 1892 |
+
|
| 1893 |
+
if not keep_text:
|
| 1894 |
+
self.reset()
|
| 1895 |
+
|
| 1896 |
+
|
| 1897 |
+
_T = TypeVar("_T", bound=Callable[..., Coroutine[Any, Any, None]])
|
| 1898 |
+
|
| 1899 |
+
|
| 1900 |
+
def _only_one_at_a_time(coroutine: _T) -> _T:
|
| 1901 |
+
"""
|
| 1902 |
+
Decorator that only starts the coroutine only if the previous call has
|
| 1903 |
+
finished. (Used to make sure that we have only one autocompleter, auto
|
| 1904 |
+
suggestor and validator running at a time.)
|
| 1905 |
+
|
| 1906 |
+
When the coroutine raises `_Retry`, it is restarted.
|
| 1907 |
+
"""
|
| 1908 |
+
running = False
|
| 1909 |
+
|
| 1910 |
+
@wraps(coroutine)
|
| 1911 |
+
async def new_coroutine(*a: Any, **kw: Any) -> Any:
|
| 1912 |
+
nonlocal running
|
| 1913 |
+
|
| 1914 |
+
# Don't start a new function, if the previous is still in progress.
|
| 1915 |
+
if running:
|
| 1916 |
+
return
|
| 1917 |
+
|
| 1918 |
+
running = True
|
| 1919 |
+
|
| 1920 |
+
try:
|
| 1921 |
+
while True:
|
| 1922 |
+
try:
|
| 1923 |
+
await coroutine(*a, **kw)
|
| 1924 |
+
except _Retry:
|
| 1925 |
+
continue
|
| 1926 |
+
else:
|
| 1927 |
+
return None
|
| 1928 |
+
finally:
|
| 1929 |
+
running = False
|
| 1930 |
+
|
| 1931 |
+
return cast(_T, new_coroutine)
|
| 1932 |
+
|
| 1933 |
+
|
| 1934 |
+
class _Retry(Exception):
|
| 1935 |
+
"Retry in `_only_one_at_a_time`."
|
| 1936 |
+
|
| 1937 |
+
|
| 1938 |
+
def indent(buffer: Buffer, from_row: int, to_row: int, count: int = 1) -> None:
|
| 1939 |
+
"""
|
| 1940 |
+
Indent text of a :class:`.Buffer` object.
|
| 1941 |
+
"""
|
| 1942 |
+
current_row = buffer.document.cursor_position_row
|
| 1943 |
+
current_col = buffer.document.cursor_position_col
|
| 1944 |
+
line_range = range(from_row, to_row)
|
| 1945 |
+
|
| 1946 |
+
# Apply transformation.
|
| 1947 |
+
indent_content = " " * count
|
| 1948 |
+
new_text = buffer.transform_lines(line_range, lambda l: indent_content + l)
|
| 1949 |
+
buffer.document = Document(
|
| 1950 |
+
new_text, Document(new_text).translate_row_col_to_index(current_row, 0)
|
| 1951 |
+
)
|
| 1952 |
+
|
| 1953 |
+
# Place cursor in the same position in text after indenting
|
| 1954 |
+
buffer.cursor_position += current_col + len(indent_content)
|
| 1955 |
+
|
| 1956 |
+
|
| 1957 |
+
def unindent(buffer: Buffer, from_row: int, to_row: int, count: int = 1) -> None:
|
| 1958 |
+
"""
|
| 1959 |
+
Unindent text of a :class:`.Buffer` object.
|
| 1960 |
+
"""
|
| 1961 |
+
current_row = buffer.document.cursor_position_row
|
| 1962 |
+
current_col = buffer.document.cursor_position_col
|
| 1963 |
+
line_range = range(from_row, to_row)
|
| 1964 |
+
|
| 1965 |
+
indent_content = " " * count
|
| 1966 |
+
|
| 1967 |
+
def transform(text: str) -> str:
|
| 1968 |
+
remove = indent_content
|
| 1969 |
+
if text.startswith(remove):
|
| 1970 |
+
return text[len(remove) :]
|
| 1971 |
+
else:
|
| 1972 |
+
return text.lstrip()
|
| 1973 |
+
|
| 1974 |
+
# Apply transformation.
|
| 1975 |
+
new_text = buffer.transform_lines(line_range, transform)
|
| 1976 |
+
buffer.document = Document(
|
| 1977 |
+
new_text, Document(new_text).translate_row_col_to_index(current_row, 0)
|
| 1978 |
+
)
|
| 1979 |
+
|
| 1980 |
+
# Place cursor in the same position in text after dedent
|
| 1981 |
+
buffer.cursor_position += current_col - len(indent_content)
|
| 1982 |
+
|
| 1983 |
+
|
| 1984 |
+
def reshape_text(buffer: Buffer, from_row: int, to_row: int) -> None:
|
| 1985 |
+
"""
|
| 1986 |
+
Reformat text, taking the width into account.
|
| 1987 |
+
`to_row` is included.
|
| 1988 |
+
(Vi 'gq' operator.)
|
| 1989 |
+
"""
|
| 1990 |
+
lines = buffer.text.splitlines(True)
|
| 1991 |
+
lines_before = lines[:from_row]
|
| 1992 |
+
lines_after = lines[to_row + 1 :]
|
| 1993 |
+
lines_to_reformat = lines[from_row : to_row + 1]
|
| 1994 |
+
|
| 1995 |
+
if lines_to_reformat:
|
| 1996 |
+
# Take indentation from the first line.
|
| 1997 |
+
match = re.search(r"^\s*", lines_to_reformat[0])
|
| 1998 |
+
length = match.end() if match else 0 # `match` can't be None, actually.
|
| 1999 |
+
|
| 2000 |
+
indent = lines_to_reformat[0][:length].replace("\n", "")
|
| 2001 |
+
|
| 2002 |
+
# Now, take all the 'words' from the lines to be reshaped.
|
| 2003 |
+
words = "".join(lines_to_reformat).split()
|
| 2004 |
+
|
| 2005 |
+
# And reshape.
|
| 2006 |
+
width = (buffer.text_width or 80) - len(indent)
|
| 2007 |
+
reshaped_text = [indent]
|
| 2008 |
+
current_width = 0
|
| 2009 |
+
for w in words:
|
| 2010 |
+
if current_width:
|
| 2011 |
+
if len(w) + current_width + 1 > width:
|
| 2012 |
+
reshaped_text.append("\n")
|
| 2013 |
+
reshaped_text.append(indent)
|
| 2014 |
+
current_width = 0
|
| 2015 |
+
else:
|
| 2016 |
+
reshaped_text.append(" ")
|
| 2017 |
+
current_width += 1
|
| 2018 |
+
|
| 2019 |
+
reshaped_text.append(w)
|
| 2020 |
+
current_width += len(w)
|
| 2021 |
+
|
| 2022 |
+
if reshaped_text[-1] != "\n":
|
| 2023 |
+
reshaped_text.append("\n")
|
| 2024 |
+
|
| 2025 |
+
# Apply result.
|
| 2026 |
+
buffer.document = Document(
|
| 2027 |
+
text="".join(lines_before + reshaped_text + lines_after),
|
| 2028 |
+
cursor_position=len("".join(lines_before + reshaped_text)),
|
| 2029 |
+
)
|
lib/python3.12/site-packages/prompt_toolkit/cache.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections import deque
|
| 4 |
+
from functools import wraps
|
| 5 |
+
from typing import Any, Callable, Dict, Generic, Hashable, Tuple, TypeVar, cast
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"SimpleCache",
|
| 9 |
+
"FastDictCache",
|
| 10 |
+
"memoized",
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
_T = TypeVar("_T", bound=Hashable)
|
| 14 |
+
_U = TypeVar("_U")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class SimpleCache(Generic[_T, _U]):
|
| 18 |
+
"""
|
| 19 |
+
Very simple cache that discards the oldest item when the cache size is
|
| 20 |
+
exceeded.
|
| 21 |
+
|
| 22 |
+
:param maxsize: Maximum size of the cache. (Don't make it too big.)
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, maxsize: int = 8) -> None:
|
| 26 |
+
assert maxsize > 0
|
| 27 |
+
|
| 28 |
+
self._data: dict[_T, _U] = {}
|
| 29 |
+
self._keys: deque[_T] = deque()
|
| 30 |
+
self.maxsize: int = maxsize
|
| 31 |
+
|
| 32 |
+
def get(self, key: _T, getter_func: Callable[[], _U]) -> _U:
|
| 33 |
+
"""
|
| 34 |
+
Get object from the cache.
|
| 35 |
+
If not found, call `getter_func` to resolve it, and put that on the top
|
| 36 |
+
of the cache instead.
|
| 37 |
+
"""
|
| 38 |
+
# Look in cache first.
|
| 39 |
+
try:
|
| 40 |
+
return self._data[key]
|
| 41 |
+
except KeyError:
|
| 42 |
+
# Not found? Get it.
|
| 43 |
+
value = getter_func()
|
| 44 |
+
self._data[key] = value
|
| 45 |
+
self._keys.append(key)
|
| 46 |
+
|
| 47 |
+
# Remove the oldest key when the size is exceeded.
|
| 48 |
+
if len(self._data) > self.maxsize:
|
| 49 |
+
key_to_remove = self._keys.popleft()
|
| 50 |
+
if key_to_remove in self._data:
|
| 51 |
+
del self._data[key_to_remove]
|
| 52 |
+
|
| 53 |
+
return value
|
| 54 |
+
|
| 55 |
+
def clear(self) -> None:
|
| 56 |
+
"Clear cache."
|
| 57 |
+
self._data = {}
|
| 58 |
+
self._keys = deque()
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
_K = TypeVar("_K", bound=Tuple[Hashable, ...])
|
| 62 |
+
_V = TypeVar("_V")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class FastDictCache(Dict[_K, _V]):
|
| 66 |
+
"""
|
| 67 |
+
Fast, lightweight cache which keeps at most `size` items.
|
| 68 |
+
It will discard the oldest items in the cache first.
|
| 69 |
+
|
| 70 |
+
The cache is a dictionary, which doesn't keep track of access counts.
|
| 71 |
+
It is perfect to cache little immutable objects which are not expensive to
|
| 72 |
+
create, but where a dictionary lookup is still much faster than an object
|
| 73 |
+
instantiation.
|
| 74 |
+
|
| 75 |
+
:param get_value: Callable that's called in case of a missing key.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
# NOTE: This cache is used to cache `prompt_toolkit.layout.screen.Char` and
|
| 79 |
+
# `prompt_toolkit.Document`. Make sure to keep this really lightweight.
|
| 80 |
+
# Accessing the cache should stay faster than instantiating new
|
| 81 |
+
# objects.
|
| 82 |
+
# (Dictionary lookups are really fast.)
|
| 83 |
+
# SimpleCache is still required for cases where the cache key is not
|
| 84 |
+
# the same as the arguments given to the function that creates the
|
| 85 |
+
# value.)
|
| 86 |
+
def __init__(self, get_value: Callable[..., _V], size: int = 1000000) -> None:
|
| 87 |
+
assert size > 0
|
| 88 |
+
|
| 89 |
+
self._keys: deque[_K] = deque()
|
| 90 |
+
self.get_value = get_value
|
| 91 |
+
self.size = size
|
| 92 |
+
|
| 93 |
+
def __missing__(self, key: _K) -> _V:
|
| 94 |
+
# Remove the oldest key when the size is exceeded.
|
| 95 |
+
if len(self) > self.size:
|
| 96 |
+
key_to_remove = self._keys.popleft()
|
| 97 |
+
if key_to_remove in self:
|
| 98 |
+
del self[key_to_remove]
|
| 99 |
+
|
| 100 |
+
result = self.get_value(*key)
|
| 101 |
+
self[key] = result
|
| 102 |
+
self._keys.append(key)
|
| 103 |
+
return result
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
_F = TypeVar("_F", bound=Callable[..., object])
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def memoized(maxsize: int = 1024) -> Callable[[_F], _F]:
|
| 110 |
+
"""
|
| 111 |
+
Memoization decorator for immutable classes and pure functions.
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
def decorator(obj: _F) -> _F:
|
| 115 |
+
cache: SimpleCache[Hashable, Any] = SimpleCache(maxsize=maxsize)
|
| 116 |
+
|
| 117 |
+
@wraps(obj)
|
| 118 |
+
def new_callable(*a: Any, **kw: Any) -> Any:
|
| 119 |
+
def create_new() -> Any:
|
| 120 |
+
return obj(*a, **kw)
|
| 121 |
+
|
| 122 |
+
key = (a, tuple(sorted(kw.items())))
|
| 123 |
+
return cache.get(key, create_new)
|
| 124 |
+
|
| 125 |
+
return cast(_F, new_callable)
|
| 126 |
+
|
| 127 |
+
return decorator
|
lib/python3.12/site-packages/prompt_toolkit/contrib/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/prompt_toolkit/contrib/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (201 Bytes). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/completers/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .system import SystemCompleter
|
| 4 |
+
|
| 5 |
+
__all__ = ["SystemCompleter"]
|
lib/python3.12/site-packages/prompt_toolkit/contrib/completers/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (341 Bytes). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/completers/__pycache__/system.cpython-312.pyc
ADDED
|
Binary file (2.68 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/completers/system.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from prompt_toolkit.completion.filesystem import ExecutableCompleter, PathCompleter
|
| 4 |
+
from prompt_toolkit.contrib.regular_languages.compiler import compile
|
| 5 |
+
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"SystemCompleter",
|
| 9 |
+
]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class SystemCompleter(GrammarCompleter):
|
| 13 |
+
"""
|
| 14 |
+
Completer for system commands.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(self) -> None:
|
| 18 |
+
# Compile grammar.
|
| 19 |
+
g = compile(
|
| 20 |
+
r"""
|
| 21 |
+
# First we have an executable.
|
| 22 |
+
(?P<executable>[^\s]+)
|
| 23 |
+
|
| 24 |
+
# Ignore literals in between.
|
| 25 |
+
(
|
| 26 |
+
\s+
|
| 27 |
+
("[^"]*" | '[^']*' | [^'"]+ )
|
| 28 |
+
)*
|
| 29 |
+
|
| 30 |
+
\s+
|
| 31 |
+
|
| 32 |
+
# Filename as parameters.
|
| 33 |
+
(
|
| 34 |
+
(?P<filename>[^\s]+) |
|
| 35 |
+
"(?P<double_quoted_filename>[^\s]+)" |
|
| 36 |
+
'(?P<single_quoted_filename>[^\s]+)'
|
| 37 |
+
)
|
| 38 |
+
""",
|
| 39 |
+
escape_funcs={
|
| 40 |
+
"double_quoted_filename": (lambda string: string.replace('"', '\\"')),
|
| 41 |
+
"single_quoted_filename": (lambda string: string.replace("'", "\\'")),
|
| 42 |
+
},
|
| 43 |
+
unescape_funcs={
|
| 44 |
+
"double_quoted_filename": (
|
| 45 |
+
lambda string: string.replace('\\"', '"')
|
| 46 |
+
), # XXX: not entirely correct.
|
| 47 |
+
"single_quoted_filename": (lambda string: string.replace("\\'", "'")),
|
| 48 |
+
},
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Create GrammarCompleter
|
| 52 |
+
super().__init__(
|
| 53 |
+
g,
|
| 54 |
+
{
|
| 55 |
+
"executable": ExecutableCompleter(),
|
| 56 |
+
"filename": PathCompleter(only_directories=False, expanduser=True),
|
| 57 |
+
"double_quoted_filename": PathCompleter(
|
| 58 |
+
only_directories=False, expanduser=True
|
| 59 |
+
),
|
| 60 |
+
"single_quoted_filename": PathCompleter(
|
| 61 |
+
only_directories=False, expanduser=True
|
| 62 |
+
),
|
| 63 |
+
},
|
| 64 |
+
)
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""
|
| 2 |
+
Tool for expressing the grammar of an input as a regular language.
|
| 3 |
+
==================================================================
|
| 4 |
+
|
| 5 |
+
The grammar for the input of many simple command line interfaces can be
|
| 6 |
+
expressed by a regular language. Examples are PDB (the Python debugger); a
|
| 7 |
+
simple (bash-like) shell with "pwd", "cd", "cat" and "ls" commands; arguments
|
| 8 |
+
that you can pass to an executable; etc. It is possible to use regular
|
| 9 |
+
expressions for validation and parsing of such a grammar. (More about regular
|
| 10 |
+
languages: http://en.wikipedia.org/wiki/Regular_language)
|
| 11 |
+
|
| 12 |
+
Example
|
| 13 |
+
-------
|
| 14 |
+
|
| 15 |
+
Let's take the pwd/cd/cat/ls example. We want to have a shell that accepts
|
| 16 |
+
these three commands. "cd" is followed by a quoted directory name and "cat" is
|
| 17 |
+
followed by a quoted file name. (We allow quotes inside the filename when
|
| 18 |
+
they're escaped with a backslash.) We could define the grammar using the
|
| 19 |
+
following regular expression::
|
| 20 |
+
|
| 21 |
+
grammar = \s* (
|
| 22 |
+
pwd |
|
| 23 |
+
ls |
|
| 24 |
+
(cd \s+ " ([^"]|\.)+ ") |
|
| 25 |
+
(cat \s+ " ([^"]|\.)+ ")
|
| 26 |
+
) \s*
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
What can we do with this grammar?
|
| 30 |
+
---------------------------------
|
| 31 |
+
|
| 32 |
+
- Syntax highlighting: We could use this for instance to give file names
|
| 33 |
+
different color.
|
| 34 |
+
- Parse the result: .. We can extract the file names and commands by using a
|
| 35 |
+
regular expression with named groups.
|
| 36 |
+
- Input validation: .. Don't accept anything that does not match this grammar.
|
| 37 |
+
When combined with a parser, we can also recursively do
|
| 38 |
+
filename validation (and accept only existing files.)
|
| 39 |
+
- Autocompletion: .... Each part of the grammar can have its own autocompleter.
|
| 40 |
+
"cat" has to be completed using file names, while "cd"
|
| 41 |
+
has to be completed using directory names.
|
| 42 |
+
|
| 43 |
+
How does it work?
|
| 44 |
+
-----------------
|
| 45 |
+
|
| 46 |
+
As a user of this library, you have to define the grammar of the input as a
|
| 47 |
+
regular expression. The parts of this grammar where autocompletion, validation
|
| 48 |
+
or any other processing is required need to be marked using a regex named
|
| 49 |
+
group. Like ``(?P<varname>...)`` for instance.
|
| 50 |
+
|
| 51 |
+
When the input is processed for validation (for instance), the regex will
|
| 52 |
+
execute, the named group is captured, and the validator associated with this
|
| 53 |
+
named group will test the captured string.
|
| 54 |
+
|
| 55 |
+
There is one tricky bit:
|
| 56 |
+
|
| 57 |
+
Often we operate on incomplete input (this is by definition the case for
|
| 58 |
+
autocompletion) and we have to decide for the cursor position in which
|
| 59 |
+
possible state the grammar it could be and in which way variables could be
|
| 60 |
+
matched up to that point.
|
| 61 |
+
|
| 62 |
+
To solve this problem, the compiler takes the original regular expression and
|
| 63 |
+
translates it into a set of other regular expressions which each match certain
|
| 64 |
+
prefixes of the original regular expression. We generate one prefix regular
|
| 65 |
+
expression for every named variable (with this variable being the end of that
|
| 66 |
+
expression).
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
TODO: some examples of:
|
| 70 |
+
- How to create a highlighter from this grammar.
|
| 71 |
+
- How to create a validator from this grammar.
|
| 72 |
+
- How to create an autocompleter from this grammar.
|
| 73 |
+
- How to create a parser from this grammar.
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
from __future__ import annotations
|
| 77 |
+
|
| 78 |
+
from .compiler import compile
|
| 79 |
+
|
| 80 |
+
__all__ = ["compile"]
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (3.55 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/compiler.cpython-312.pyc
ADDED
|
Binary file (25.5 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/completion.cpython-312.pyc
ADDED
|
Binary file (4.49 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/lexer.cpython-312.pyc
ADDED
|
Binary file (4.37 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/regex_parser.cpython-312.pyc
ADDED
|
Binary file (11.2 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/validation.cpython-312.pyc
ADDED
|
Binary file (2.59 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/compiler.py
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""
|
| 2 |
+
Compiler for a regular grammar.
|
| 3 |
+
|
| 4 |
+
Example usage::
|
| 5 |
+
|
| 6 |
+
# Create and compile grammar.
|
| 7 |
+
p = compile('add \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)')
|
| 8 |
+
|
| 9 |
+
# Match input string.
|
| 10 |
+
m = p.match('add 23 432')
|
| 11 |
+
|
| 12 |
+
# Get variables.
|
| 13 |
+
m.variables().get('var1') # Returns "23"
|
| 14 |
+
m.variables().get('var2') # Returns "432"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
Partial matches are possible::
|
| 18 |
+
|
| 19 |
+
# Create and compile grammar.
|
| 20 |
+
p = compile('''
|
| 21 |
+
# Operators with two arguments.
|
| 22 |
+
((?P<operator1>[^\s]+) \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)) |
|
| 23 |
+
|
| 24 |
+
# Operators with only one arguments.
|
| 25 |
+
((?P<operator2>[^\s]+) \s+ (?P<var1>[^\s]+))
|
| 26 |
+
''')
|
| 27 |
+
|
| 28 |
+
# Match partial input string.
|
| 29 |
+
m = p.match_prefix('add 23')
|
| 30 |
+
|
| 31 |
+
# Get variables. (Notice that both operator1 and operator2 contain the
|
| 32 |
+
# value "add".) This is because our input is incomplete, and we don't know
|
| 33 |
+
# yet in which rule of the regex we we'll end up. It could also be that
|
| 34 |
+
# `operator1` and `operator2` have a different autocompleter and we want to
|
| 35 |
+
# call all possible autocompleters that would result in valid input.)
|
| 36 |
+
m.variables().get('var1') # Returns "23"
|
| 37 |
+
m.variables().get('operator1') # Returns "add"
|
| 38 |
+
m.variables().get('operator2') # Returns "add"
|
| 39 |
+
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
from __future__ import annotations
|
| 43 |
+
|
| 44 |
+
import re
|
| 45 |
+
from typing import Callable, Dict, Iterable, Iterator, Pattern, TypeVar, overload
|
| 46 |
+
from typing import Match as RegexMatch
|
| 47 |
+
|
| 48 |
+
from .regex_parser import (
|
| 49 |
+
AnyNode,
|
| 50 |
+
Lookahead,
|
| 51 |
+
Node,
|
| 52 |
+
NodeSequence,
|
| 53 |
+
Regex,
|
| 54 |
+
Repeat,
|
| 55 |
+
Variable,
|
| 56 |
+
parse_regex,
|
| 57 |
+
tokenize_regex,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
__all__ = ["compile", "Match", "Variables"]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# Name of the named group in the regex, matching trailing input.
|
| 64 |
+
# (Trailing input is when the input contains characters after the end of the
|
| 65 |
+
# expression has been matched.)
|
| 66 |
+
_INVALID_TRAILING_INPUT = "invalid_trailing"
|
| 67 |
+
|
| 68 |
+
EscapeFuncDict = Dict[str, Callable[[str], str]]
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class _CompiledGrammar:
|
| 72 |
+
"""
|
| 73 |
+
Compiles a grammar. This will take the parse tree of a regular expression
|
| 74 |
+
and compile the grammar.
|
| 75 |
+
|
| 76 |
+
:param root_node: :class~`.regex_parser.Node` instance.
|
| 77 |
+
:param escape_funcs: `dict` mapping variable names to escape callables.
|
| 78 |
+
:param unescape_funcs: `dict` mapping variable names to unescape callables.
|
| 79 |
+
"""
|
| 80 |
+
|
| 81 |
+
def __init__(
|
| 82 |
+
self,
|
| 83 |
+
root_node: Node,
|
| 84 |
+
escape_funcs: EscapeFuncDict | None = None,
|
| 85 |
+
unescape_funcs: EscapeFuncDict | None = None,
|
| 86 |
+
) -> None:
|
| 87 |
+
self.root_node = root_node
|
| 88 |
+
self.escape_funcs = escape_funcs or {}
|
| 89 |
+
self.unescape_funcs = unescape_funcs or {}
|
| 90 |
+
|
| 91 |
+
#: Dictionary that will map the regex names to Node instances.
|
| 92 |
+
self._group_names_to_nodes: dict[
|
| 93 |
+
str, str
|
| 94 |
+
] = {} # Maps regex group names to varnames.
|
| 95 |
+
counter = [0]
|
| 96 |
+
|
| 97 |
+
def create_group_func(node: Variable) -> str:
|
| 98 |
+
name = f"n{counter[0]}"
|
| 99 |
+
self._group_names_to_nodes[name] = node.varname
|
| 100 |
+
counter[0] += 1
|
| 101 |
+
return name
|
| 102 |
+
|
| 103 |
+
# Compile regex strings.
|
| 104 |
+
self._re_pattern = f"^{self._transform(root_node, create_group_func)}$"
|
| 105 |
+
self._re_prefix_patterns = list(
|
| 106 |
+
self._transform_prefix(root_node, create_group_func)
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# Compile the regex itself.
|
| 110 |
+
flags = re.DOTALL # Note that we don't need re.MULTILINE! (^ and $
|
| 111 |
+
# still represent the start and end of input text.)
|
| 112 |
+
self._re = re.compile(self._re_pattern, flags)
|
| 113 |
+
self._re_prefix = [re.compile(t, flags) for t in self._re_prefix_patterns]
|
| 114 |
+
|
| 115 |
+
# We compile one more set of regexes, similar to `_re_prefix`, but accept any trailing
|
| 116 |
+
# input. This will ensure that we can still highlight the input correctly, even when the
|
| 117 |
+
# input contains some additional characters at the end that don't match the grammar.)
|
| 118 |
+
self._re_prefix_with_trailing_input = [
|
| 119 |
+
re.compile(
|
| 120 |
+
r"(?:{})(?P<{}>.*?)$".format(t.rstrip("$"), _INVALID_TRAILING_INPUT),
|
| 121 |
+
flags,
|
| 122 |
+
)
|
| 123 |
+
for t in self._re_prefix_patterns
|
| 124 |
+
]
|
| 125 |
+
|
| 126 |
+
def escape(self, varname: str, value: str) -> str:
|
| 127 |
+
"""
|
| 128 |
+
Escape `value` to fit in the place of this variable into the grammar.
|
| 129 |
+
"""
|
| 130 |
+
f = self.escape_funcs.get(varname)
|
| 131 |
+
return f(value) if f else value
|
| 132 |
+
|
| 133 |
+
def unescape(self, varname: str, value: str) -> str:
|
| 134 |
+
"""
|
| 135 |
+
Unescape `value`.
|
| 136 |
+
"""
|
| 137 |
+
f = self.unescape_funcs.get(varname)
|
| 138 |
+
return f(value) if f else value
|
| 139 |
+
|
| 140 |
+
@classmethod
|
| 141 |
+
def _transform(
|
| 142 |
+
cls, root_node: Node, create_group_func: Callable[[Variable], str]
|
| 143 |
+
) -> str:
|
| 144 |
+
"""
|
| 145 |
+
Turn a :class:`Node` object into a regular expression.
|
| 146 |
+
|
| 147 |
+
:param root_node: The :class:`Node` instance for which we generate the grammar.
|
| 148 |
+
:param create_group_func: A callable which takes a `Node` and returns the next
|
| 149 |
+
free name for this node.
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
def transform(node: Node) -> str:
|
| 153 |
+
# Turn `AnyNode` into an OR.
|
| 154 |
+
if isinstance(node, AnyNode):
|
| 155 |
+
return "(?:{})".format("|".join(transform(c) for c in node.children))
|
| 156 |
+
|
| 157 |
+
# Concatenate a `NodeSequence`
|
| 158 |
+
elif isinstance(node, NodeSequence):
|
| 159 |
+
return "".join(transform(c) for c in node.children)
|
| 160 |
+
|
| 161 |
+
# For Regex and Lookahead nodes, just insert them literally.
|
| 162 |
+
elif isinstance(node, Regex):
|
| 163 |
+
return node.regex
|
| 164 |
+
|
| 165 |
+
elif isinstance(node, Lookahead):
|
| 166 |
+
before = "(?!" if node.negative else "(="
|
| 167 |
+
return before + transform(node.childnode) + ")"
|
| 168 |
+
|
| 169 |
+
# A `Variable` wraps the children into a named group.
|
| 170 |
+
elif isinstance(node, Variable):
|
| 171 |
+
return f"(?P<{create_group_func(node)}>{transform(node.childnode)})"
|
| 172 |
+
|
| 173 |
+
# `Repeat`.
|
| 174 |
+
elif isinstance(node, Repeat):
|
| 175 |
+
if node.max_repeat is None:
|
| 176 |
+
if node.min_repeat == 0:
|
| 177 |
+
repeat_sign = "*"
|
| 178 |
+
elif node.min_repeat == 1:
|
| 179 |
+
repeat_sign = "+"
|
| 180 |
+
else:
|
| 181 |
+
repeat_sign = "{%i,%s}" % (
|
| 182 |
+
node.min_repeat,
|
| 183 |
+
("" if node.max_repeat is None else str(node.max_repeat)),
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
return "(?:{}){}{}".format(
|
| 187 |
+
transform(node.childnode),
|
| 188 |
+
repeat_sign,
|
| 189 |
+
("" if node.greedy else "?"),
|
| 190 |
+
)
|
| 191 |
+
else:
|
| 192 |
+
raise TypeError(f"Got {node!r}")
|
| 193 |
+
|
| 194 |
+
return transform(root_node)
|
| 195 |
+
|
| 196 |
+
@classmethod
|
| 197 |
+
def _transform_prefix(
|
| 198 |
+
cls, root_node: Node, create_group_func: Callable[[Variable], str]
|
| 199 |
+
) -> Iterable[str]:
|
| 200 |
+
"""
|
| 201 |
+
Yield all the regular expressions matching a prefix of the grammar
|
| 202 |
+
defined by the `Node` instance.
|
| 203 |
+
|
| 204 |
+
For each `Variable`, one regex pattern will be generated, with this
|
| 205 |
+
named group at the end. This is required because a regex engine will
|
| 206 |
+
terminate once a match is found. For autocompletion however, we need
|
| 207 |
+
the matches for all possible paths, so that we can provide completions
|
| 208 |
+
for each `Variable`.
|
| 209 |
+
|
| 210 |
+
- So, in the case of an `Any` (`A|B|C)', we generate a pattern for each
|
| 211 |
+
clause. This is one for `A`, one for `B` and one for `C`. Unless some
|
| 212 |
+
groups don't contain a `Variable`, then these can be merged together.
|
| 213 |
+
- In the case of a `NodeSequence` (`ABC`), we generate a pattern for
|
| 214 |
+
each prefix that ends with a variable, and one pattern for the whole
|
| 215 |
+
sequence. So, that's one for `A`, one for `AB` and one for `ABC`.
|
| 216 |
+
|
| 217 |
+
:param root_node: The :class:`Node` instance for which we generate the grammar.
|
| 218 |
+
:param create_group_func: A callable which takes a `Node` and returns the next
|
| 219 |
+
free name for this node.
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
def contains_variable(node: Node) -> bool:
|
| 223 |
+
if isinstance(node, Regex):
|
| 224 |
+
return False
|
| 225 |
+
elif isinstance(node, Variable):
|
| 226 |
+
return True
|
| 227 |
+
elif isinstance(node, (Lookahead, Repeat)):
|
| 228 |
+
return contains_variable(node.childnode)
|
| 229 |
+
elif isinstance(node, (NodeSequence, AnyNode)):
|
| 230 |
+
return any(contains_variable(child) for child in node.children)
|
| 231 |
+
|
| 232 |
+
return False
|
| 233 |
+
|
| 234 |
+
def transform(node: Node) -> Iterable[str]:
|
| 235 |
+
# Generate separate pattern for all terms that contain variables
|
| 236 |
+
# within this OR. Terms that don't contain a variable can be merged
|
| 237 |
+
# together in one pattern.
|
| 238 |
+
if isinstance(node, AnyNode):
|
| 239 |
+
# If we have a definition like:
|
| 240 |
+
# (?P<name> .*) | (?P<city> .*)
|
| 241 |
+
# Then we want to be able to generate completions for both the
|
| 242 |
+
# name as well as the city. We do this by yielding two
|
| 243 |
+
# different regular expressions, because the engine won't
|
| 244 |
+
# follow multiple paths, if multiple are possible.
|
| 245 |
+
children_with_variable = []
|
| 246 |
+
children_without_variable = []
|
| 247 |
+
for c in node.children:
|
| 248 |
+
if contains_variable(c):
|
| 249 |
+
children_with_variable.append(c)
|
| 250 |
+
else:
|
| 251 |
+
children_without_variable.append(c)
|
| 252 |
+
|
| 253 |
+
for c in children_with_variable:
|
| 254 |
+
yield from transform(c)
|
| 255 |
+
|
| 256 |
+
# Merge options without variable together.
|
| 257 |
+
if children_without_variable:
|
| 258 |
+
yield "|".join(
|
| 259 |
+
r for c in children_without_variable for r in transform(c)
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
# For a sequence, generate a pattern for each prefix that ends with
|
| 263 |
+
# a variable + one pattern of the complete sequence.
|
| 264 |
+
# (This is because, for autocompletion, we match the text before
|
| 265 |
+
# the cursor, and completions are given for the variable that we
|
| 266 |
+
# match right before the cursor.)
|
| 267 |
+
elif isinstance(node, NodeSequence):
|
| 268 |
+
# For all components in the sequence, compute prefix patterns,
|
| 269 |
+
# as well as full patterns.
|
| 270 |
+
complete = [cls._transform(c, create_group_func) for c in node.children]
|
| 271 |
+
prefixes = [list(transform(c)) for c in node.children]
|
| 272 |
+
variable_nodes = [contains_variable(c) for c in node.children]
|
| 273 |
+
|
| 274 |
+
# If any child is contains a variable, we should yield a
|
| 275 |
+
# pattern up to that point, so that we are sure this will be
|
| 276 |
+
# matched.
|
| 277 |
+
for i in range(len(node.children)):
|
| 278 |
+
if variable_nodes[i]:
|
| 279 |
+
for c_str in prefixes[i]:
|
| 280 |
+
yield "".join(complete[:i]) + c_str
|
| 281 |
+
|
| 282 |
+
# If there are non-variable nodes, merge all the prefixes into
|
| 283 |
+
# one pattern. If the input is: "[part1] [part2] [part3]", then
|
| 284 |
+
# this gets compiled into:
|
| 285 |
+
# (complete1 + (complete2 + (complete3 | partial3) | partial2) | partial1 )
|
| 286 |
+
# For nodes that contain a variable, we skip the "|partial"
|
| 287 |
+
# part here, because thees are matched with the previous
|
| 288 |
+
# patterns.
|
| 289 |
+
if not all(variable_nodes):
|
| 290 |
+
result = []
|
| 291 |
+
|
| 292 |
+
# Start with complete patterns.
|
| 293 |
+
for i in range(len(node.children)):
|
| 294 |
+
result.append("(?:")
|
| 295 |
+
result.append(complete[i])
|
| 296 |
+
|
| 297 |
+
# Add prefix patterns.
|
| 298 |
+
for i in range(len(node.children) - 1, -1, -1):
|
| 299 |
+
if variable_nodes[i]:
|
| 300 |
+
# No need to yield a prefix for this one, we did
|
| 301 |
+
# the variable prefixes earlier.
|
| 302 |
+
result.append(")")
|
| 303 |
+
else:
|
| 304 |
+
result.append("|(?:")
|
| 305 |
+
# If this yields multiple, we should yield all combinations.
|
| 306 |
+
assert len(prefixes[i]) == 1
|
| 307 |
+
result.append(prefixes[i][0])
|
| 308 |
+
result.append("))")
|
| 309 |
+
|
| 310 |
+
yield "".join(result)
|
| 311 |
+
|
| 312 |
+
elif isinstance(node, Regex):
|
| 313 |
+
yield f"(?:{node.regex})?"
|
| 314 |
+
|
| 315 |
+
elif isinstance(node, Lookahead):
|
| 316 |
+
if node.negative:
|
| 317 |
+
yield f"(?!{cls._transform(node.childnode, create_group_func)})"
|
| 318 |
+
else:
|
| 319 |
+
# Not sure what the correct semantics are in this case.
|
| 320 |
+
# (Probably it's not worth implementing this.)
|
| 321 |
+
raise Exception("Positive lookahead not yet supported.")
|
| 322 |
+
|
| 323 |
+
elif isinstance(node, Variable):
|
| 324 |
+
# (Note that we should not append a '?' here. the 'transform'
|
| 325 |
+
# method will already recursively do that.)
|
| 326 |
+
for c_str in transform(node.childnode):
|
| 327 |
+
yield f"(?P<{create_group_func(node)}>{c_str})"
|
| 328 |
+
|
| 329 |
+
elif isinstance(node, Repeat):
|
| 330 |
+
# If we have a repetition of 8 times. That would mean that the
|
| 331 |
+
# current input could have for instance 7 times a complete
|
| 332 |
+
# match, followed by a partial match.
|
| 333 |
+
prefix = cls._transform(node.childnode, create_group_func)
|
| 334 |
+
|
| 335 |
+
if node.max_repeat == 1:
|
| 336 |
+
yield from transform(node.childnode)
|
| 337 |
+
else:
|
| 338 |
+
for c_str in transform(node.childnode):
|
| 339 |
+
if node.max_repeat:
|
| 340 |
+
repeat_sign = "{,%i}" % (node.max_repeat - 1)
|
| 341 |
+
else:
|
| 342 |
+
repeat_sign = "*"
|
| 343 |
+
yield "(?:{}){}{}{}".format(
|
| 344 |
+
prefix,
|
| 345 |
+
repeat_sign,
|
| 346 |
+
("" if node.greedy else "?"),
|
| 347 |
+
c_str,
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
else:
|
| 351 |
+
raise TypeError(f"Got {node!r}")
|
| 352 |
+
|
| 353 |
+
for r in transform(root_node):
|
| 354 |
+
yield f"^(?:{r})$"
|
| 355 |
+
|
| 356 |
+
def match(self, string: str) -> Match | None:
|
| 357 |
+
"""
|
| 358 |
+
Match the string with the grammar.
|
| 359 |
+
Returns a :class:`Match` instance or `None` when the input doesn't match the grammar.
|
| 360 |
+
|
| 361 |
+
:param string: The input string.
|
| 362 |
+
"""
|
| 363 |
+
m = self._re.match(string)
|
| 364 |
+
|
| 365 |
+
if m:
|
| 366 |
+
return Match(
|
| 367 |
+
string, [(self._re, m)], self._group_names_to_nodes, self.unescape_funcs
|
| 368 |
+
)
|
| 369 |
+
return None
|
| 370 |
+
|
| 371 |
+
def match_prefix(self, string: str) -> Match | None:
|
| 372 |
+
"""
|
| 373 |
+
Do a partial match of the string with the grammar. The returned
|
| 374 |
+
:class:`Match` instance can contain multiple representations of the
|
| 375 |
+
match. This will never return `None`. If it doesn't match at all, the "trailing input"
|
| 376 |
+
part will capture all of the input.
|
| 377 |
+
|
| 378 |
+
:param string: The input string.
|
| 379 |
+
"""
|
| 380 |
+
# First try to match using `_re_prefix`. If nothing is found, use the patterns that
|
| 381 |
+
# also accept trailing characters.
|
| 382 |
+
for patterns in [self._re_prefix, self._re_prefix_with_trailing_input]:
|
| 383 |
+
matches = [(r, r.match(string)) for r in patterns]
|
| 384 |
+
matches2 = [(r, m) for r, m in matches if m]
|
| 385 |
+
|
| 386 |
+
if matches2 != []:
|
| 387 |
+
return Match(
|
| 388 |
+
string, matches2, self._group_names_to_nodes, self.unescape_funcs
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
return None
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
class Match:
|
| 395 |
+
"""
|
| 396 |
+
:param string: The input string.
|
| 397 |
+
:param re_matches: List of (compiled_re_pattern, re_match) tuples.
|
| 398 |
+
:param group_names_to_nodes: Dictionary mapping all the re group names to the matching Node instances.
|
| 399 |
+
"""
|
| 400 |
+
|
| 401 |
+
def __init__(
|
| 402 |
+
self,
|
| 403 |
+
string: str,
|
| 404 |
+
re_matches: list[tuple[Pattern[str], RegexMatch[str]]],
|
| 405 |
+
group_names_to_nodes: dict[str, str],
|
| 406 |
+
unescape_funcs: dict[str, Callable[[str], str]],
|
| 407 |
+
):
|
| 408 |
+
self.string = string
|
| 409 |
+
self._re_matches = re_matches
|
| 410 |
+
self._group_names_to_nodes = group_names_to_nodes
|
| 411 |
+
self._unescape_funcs = unescape_funcs
|
| 412 |
+
|
| 413 |
+
def _nodes_to_regs(self) -> list[tuple[str, tuple[int, int]]]:
|
| 414 |
+
"""
|
| 415 |
+
Return a list of (varname, reg) tuples.
|
| 416 |
+
"""
|
| 417 |
+
|
| 418 |
+
def get_tuples() -> Iterable[tuple[str, tuple[int, int]]]:
|
| 419 |
+
for r, re_match in self._re_matches:
|
| 420 |
+
for group_name, group_index in r.groupindex.items():
|
| 421 |
+
if group_name != _INVALID_TRAILING_INPUT:
|
| 422 |
+
regs = re_match.regs
|
| 423 |
+
reg = regs[group_index]
|
| 424 |
+
node = self._group_names_to_nodes[group_name]
|
| 425 |
+
yield (node, reg)
|
| 426 |
+
|
| 427 |
+
return list(get_tuples())
|
| 428 |
+
|
| 429 |
+
def _nodes_to_values(self) -> list[tuple[str, str, tuple[int, int]]]:
|
| 430 |
+
"""
|
| 431 |
+
Returns list of (Node, string_value) tuples.
|
| 432 |
+
"""
|
| 433 |
+
|
| 434 |
+
def is_none(sl: tuple[int, int]) -> bool:
|
| 435 |
+
return sl[0] == -1 and sl[1] == -1
|
| 436 |
+
|
| 437 |
+
def get(sl: tuple[int, int]) -> str:
|
| 438 |
+
return self.string[sl[0] : sl[1]]
|
| 439 |
+
|
| 440 |
+
return [
|
| 441 |
+
(varname, get(slice), slice)
|
| 442 |
+
for varname, slice in self._nodes_to_regs()
|
| 443 |
+
if not is_none(slice)
|
| 444 |
+
]
|
| 445 |
+
|
| 446 |
+
def _unescape(self, varname: str, value: str) -> str:
|
| 447 |
+
unwrapper = self._unescape_funcs.get(varname)
|
| 448 |
+
return unwrapper(value) if unwrapper else value
|
| 449 |
+
|
| 450 |
+
def variables(self) -> Variables:
|
| 451 |
+
"""
|
| 452 |
+
Returns :class:`Variables` instance.
|
| 453 |
+
"""
|
| 454 |
+
return Variables(
|
| 455 |
+
[(k, self._unescape(k, v), sl) for k, v, sl in self._nodes_to_values()]
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
def trailing_input(self) -> MatchVariable | None:
|
| 459 |
+
"""
|
| 460 |
+
Get the `MatchVariable` instance, representing trailing input, if there is any.
|
| 461 |
+
"Trailing input" is input at the end that does not match the grammar anymore, but
|
| 462 |
+
when this is removed from the end of the input, the input would be a valid string.
|
| 463 |
+
"""
|
| 464 |
+
slices: list[tuple[int, int]] = []
|
| 465 |
+
|
| 466 |
+
# Find all regex group for the name _INVALID_TRAILING_INPUT.
|
| 467 |
+
for r, re_match in self._re_matches:
|
| 468 |
+
for group_name, group_index in r.groupindex.items():
|
| 469 |
+
if group_name == _INVALID_TRAILING_INPUT:
|
| 470 |
+
slices.append(re_match.regs[group_index])
|
| 471 |
+
|
| 472 |
+
# Take the smallest part. (Smaller trailing text means that a larger input has
|
| 473 |
+
# been matched, so that is better.)
|
| 474 |
+
if slices:
|
| 475 |
+
slice = (max(i[0] for i in slices), max(i[1] for i in slices))
|
| 476 |
+
value = self.string[slice[0] : slice[1]]
|
| 477 |
+
return MatchVariable("<trailing_input>", value, slice)
|
| 478 |
+
return None
|
| 479 |
+
|
| 480 |
+
def end_nodes(self) -> Iterable[MatchVariable]:
|
| 481 |
+
"""
|
| 482 |
+
Yields `MatchVariable` instances for all the nodes having their end
|
| 483 |
+
position at the end of the input string.
|
| 484 |
+
"""
|
| 485 |
+
for varname, reg in self._nodes_to_regs():
|
| 486 |
+
# If this part goes until the end of the input string.
|
| 487 |
+
if reg[1] == len(self.string):
|
| 488 |
+
value = self._unescape(varname, self.string[reg[0] : reg[1]])
|
| 489 |
+
yield MatchVariable(varname, value, (reg[0], reg[1]))
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
_T = TypeVar("_T")
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
class Variables:
|
| 496 |
+
def __init__(self, tuples: list[tuple[str, str, tuple[int, int]]]) -> None:
|
| 497 |
+
#: List of (varname, value, slice) tuples.
|
| 498 |
+
self._tuples = tuples
|
| 499 |
+
|
| 500 |
+
def __repr__(self) -> str:
|
| 501 |
+
return "{}({})".format(
|
| 502 |
+
self.__class__.__name__,
|
| 503 |
+
", ".join(f"{k}={v!r}" for k, v, _ in self._tuples),
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
@overload
|
| 507 |
+
def get(self, key: str) -> str | None: ...
|
| 508 |
+
|
| 509 |
+
@overload
|
| 510 |
+
def get(self, key: str, default: str | _T) -> str | _T: ...
|
| 511 |
+
|
| 512 |
+
def get(self, key: str, default: str | _T | None = None) -> str | _T | None:
|
| 513 |
+
items = self.getall(key)
|
| 514 |
+
return items[0] if items else default
|
| 515 |
+
|
| 516 |
+
def getall(self, key: str) -> list[str]:
|
| 517 |
+
return [v for k, v, _ in self._tuples if k == key]
|
| 518 |
+
|
| 519 |
+
def __getitem__(self, key: str) -> str | None:
|
| 520 |
+
return self.get(key)
|
| 521 |
+
|
| 522 |
+
def __iter__(self) -> Iterator[MatchVariable]:
|
| 523 |
+
"""
|
| 524 |
+
Yield `MatchVariable` instances.
|
| 525 |
+
"""
|
| 526 |
+
for varname, value, slice in self._tuples:
|
| 527 |
+
yield MatchVariable(varname, value, slice)
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
class MatchVariable:
|
| 531 |
+
"""
|
| 532 |
+
Represents a match of a variable in the grammar.
|
| 533 |
+
|
| 534 |
+
:param varname: (string) Name of the variable.
|
| 535 |
+
:param value: (string) Value of this variable.
|
| 536 |
+
:param slice: (start, stop) tuple, indicating the position of this variable
|
| 537 |
+
in the input string.
|
| 538 |
+
"""
|
| 539 |
+
|
| 540 |
+
def __init__(self, varname: str, value: str, slice: tuple[int, int]) -> None:
|
| 541 |
+
self.varname = varname
|
| 542 |
+
self.value = value
|
| 543 |
+
self.slice = slice
|
| 544 |
+
|
| 545 |
+
self.start = self.slice[0]
|
| 546 |
+
self.stop = self.slice[1]
|
| 547 |
+
|
| 548 |
+
def __repr__(self) -> str:
|
| 549 |
+
return f"{self.__class__.__name__}({self.varname!r}, {self.value!r})"
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
def compile(
|
| 553 |
+
expression: str,
|
| 554 |
+
escape_funcs: EscapeFuncDict | None = None,
|
| 555 |
+
unescape_funcs: EscapeFuncDict | None = None,
|
| 556 |
+
) -> _CompiledGrammar:
|
| 557 |
+
"""
|
| 558 |
+
Compile grammar (given as regex string), returning a `CompiledGrammar`
|
| 559 |
+
instance.
|
| 560 |
+
"""
|
| 561 |
+
return _compile_from_parse_tree(
|
| 562 |
+
parse_regex(tokenize_regex(expression)),
|
| 563 |
+
escape_funcs=escape_funcs,
|
| 564 |
+
unescape_funcs=unescape_funcs,
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def _compile_from_parse_tree(
|
| 569 |
+
root_node: Node,
|
| 570 |
+
escape_funcs: EscapeFuncDict | None = None,
|
| 571 |
+
unescape_funcs: EscapeFuncDict | None = None,
|
| 572 |
+
) -> _CompiledGrammar:
|
| 573 |
+
"""
|
| 574 |
+
Compile grammar (given as parse tree), returning a `CompiledGrammar`
|
| 575 |
+
instance.
|
| 576 |
+
"""
|
| 577 |
+
return _CompiledGrammar(
|
| 578 |
+
root_node, escape_funcs=escape_funcs, unescape_funcs=unescape_funcs
|
| 579 |
+
)
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/completion.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Completer for a regular grammar.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from typing import Iterable
|
| 8 |
+
|
| 9 |
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
| 10 |
+
from prompt_toolkit.document import Document
|
| 11 |
+
|
| 12 |
+
from .compiler import Match, _CompiledGrammar
|
| 13 |
+
|
| 14 |
+
__all__ = [
|
| 15 |
+
"GrammarCompleter",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class GrammarCompleter(Completer):
|
| 20 |
+
"""
|
| 21 |
+
Completer which can be used for autocompletion according to variables in
|
| 22 |
+
the grammar. Each variable can have a different autocompleter.
|
| 23 |
+
|
| 24 |
+
:param compiled_grammar: `GrammarCompleter` instance.
|
| 25 |
+
:param completers: `dict` mapping variable names of the grammar to the
|
| 26 |
+
`Completer` instances to be used for each variable.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self, compiled_grammar: _CompiledGrammar, completers: dict[str, Completer]
|
| 31 |
+
) -> None:
|
| 32 |
+
self.compiled_grammar = compiled_grammar
|
| 33 |
+
self.completers = completers
|
| 34 |
+
|
| 35 |
+
def get_completions(
|
| 36 |
+
self, document: Document, complete_event: CompleteEvent
|
| 37 |
+
) -> Iterable[Completion]:
|
| 38 |
+
m = self.compiled_grammar.match_prefix(document.text_before_cursor)
|
| 39 |
+
|
| 40 |
+
if m:
|
| 41 |
+
yield from self._remove_duplicates(
|
| 42 |
+
self._get_completions_for_match(m, complete_event)
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
def _get_completions_for_match(
|
| 46 |
+
self, match: Match, complete_event: CompleteEvent
|
| 47 |
+
) -> Iterable[Completion]:
|
| 48 |
+
"""
|
| 49 |
+
Yield all the possible completions for this input string.
|
| 50 |
+
(The completer assumes that the cursor position was at the end of the
|
| 51 |
+
input string.)
|
| 52 |
+
"""
|
| 53 |
+
for match_variable in match.end_nodes():
|
| 54 |
+
varname = match_variable.varname
|
| 55 |
+
start = match_variable.start
|
| 56 |
+
|
| 57 |
+
completer = self.completers.get(varname)
|
| 58 |
+
|
| 59 |
+
if completer:
|
| 60 |
+
text = match_variable.value
|
| 61 |
+
|
| 62 |
+
# Unwrap text.
|
| 63 |
+
unwrapped_text = self.compiled_grammar.unescape(varname, text)
|
| 64 |
+
|
| 65 |
+
# Create a document, for the completions API (text/cursor_position)
|
| 66 |
+
document = Document(unwrapped_text, len(unwrapped_text))
|
| 67 |
+
|
| 68 |
+
# Call completer
|
| 69 |
+
for completion in completer.get_completions(document, complete_event):
|
| 70 |
+
new_text = (
|
| 71 |
+
unwrapped_text[: len(text) + completion.start_position]
|
| 72 |
+
+ completion.text
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Wrap again.
|
| 76 |
+
yield Completion(
|
| 77 |
+
text=self.compiled_grammar.escape(varname, new_text),
|
| 78 |
+
start_position=start - len(match.string),
|
| 79 |
+
display=completion.display,
|
| 80 |
+
display_meta=completion.display_meta,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
def _remove_duplicates(self, items: Iterable[Completion]) -> Iterable[Completion]:
|
| 84 |
+
"""
|
| 85 |
+
Remove duplicates, while keeping the order.
|
| 86 |
+
(Sometimes we have duplicates, because the there several matches of the
|
| 87 |
+
same grammar, each yielding similar completions.)
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
def hash_completion(completion: Completion) -> tuple[str, int]:
|
| 91 |
+
return completion.text, completion.start_position
|
| 92 |
+
|
| 93 |
+
yielded_so_far: set[tuple[str, int]] = set()
|
| 94 |
+
|
| 95 |
+
for completion in items:
|
| 96 |
+
hash_value = hash_completion(completion)
|
| 97 |
+
|
| 98 |
+
if hash_value not in yielded_so_far:
|
| 99 |
+
yielded_so_far.add(hash_value)
|
| 100 |
+
yield completion
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/lexer.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
`GrammarLexer` is compatible with other lexers and can be used to highlight
|
| 3 |
+
the input using a regular grammar with annotations.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from typing import Callable
|
| 9 |
+
|
| 10 |
+
from prompt_toolkit.document import Document
|
| 11 |
+
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
|
| 12 |
+
from prompt_toolkit.formatted_text.utils import split_lines
|
| 13 |
+
from prompt_toolkit.lexers import Lexer
|
| 14 |
+
|
| 15 |
+
from .compiler import _CompiledGrammar
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
"GrammarLexer",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class GrammarLexer(Lexer):
|
| 23 |
+
"""
|
| 24 |
+
Lexer which can be used for highlighting of fragments according to variables in the grammar.
|
| 25 |
+
|
| 26 |
+
(It does not actual lexing of the string, but it exposes an API, compatible
|
| 27 |
+
with the Pygments lexer class.)
|
| 28 |
+
|
| 29 |
+
:param compiled_grammar: Grammar as returned by the `compile()` function.
|
| 30 |
+
:param lexers: Dictionary mapping variable names of the regular grammar to
|
| 31 |
+
the lexers that should be used for this part. (This can
|
| 32 |
+
call other lexers recursively.) If you wish a part of the
|
| 33 |
+
grammar to just get one fragment, use a
|
| 34 |
+
`prompt_toolkit.lexers.SimpleLexer`.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
compiled_grammar: _CompiledGrammar,
|
| 40 |
+
default_style: str = "",
|
| 41 |
+
lexers: dict[str, Lexer] | None = None,
|
| 42 |
+
) -> None:
|
| 43 |
+
self.compiled_grammar = compiled_grammar
|
| 44 |
+
self.default_style = default_style
|
| 45 |
+
self.lexers = lexers or {}
|
| 46 |
+
|
| 47 |
+
def _get_text_fragments(self, text: str) -> StyleAndTextTuples:
|
| 48 |
+
m = self.compiled_grammar.match_prefix(text)
|
| 49 |
+
|
| 50 |
+
if m:
|
| 51 |
+
characters: StyleAndTextTuples = [(self.default_style, c) for c in text]
|
| 52 |
+
|
| 53 |
+
for v in m.variables():
|
| 54 |
+
# If we have a `Lexer` instance for this part of the input.
|
| 55 |
+
# Tokenize recursively and apply tokens.
|
| 56 |
+
lexer = self.lexers.get(v.varname)
|
| 57 |
+
|
| 58 |
+
if lexer:
|
| 59 |
+
document = Document(text[v.start : v.stop])
|
| 60 |
+
lexer_tokens_for_line = lexer.lex_document(document)
|
| 61 |
+
text_fragments: StyleAndTextTuples = []
|
| 62 |
+
for i in range(len(document.lines)):
|
| 63 |
+
text_fragments.extend(lexer_tokens_for_line(i))
|
| 64 |
+
text_fragments.append(("", "\n"))
|
| 65 |
+
if text_fragments:
|
| 66 |
+
text_fragments.pop()
|
| 67 |
+
|
| 68 |
+
i = v.start
|
| 69 |
+
for t, s, *_ in text_fragments:
|
| 70 |
+
for c in s:
|
| 71 |
+
if characters[i][0] == self.default_style:
|
| 72 |
+
characters[i] = (t, characters[i][1])
|
| 73 |
+
i += 1
|
| 74 |
+
|
| 75 |
+
# Highlight trailing input.
|
| 76 |
+
trailing_input = m.trailing_input()
|
| 77 |
+
if trailing_input:
|
| 78 |
+
for i in range(trailing_input.start, trailing_input.stop):
|
| 79 |
+
characters[i] = ("class:trailing-input", characters[i][1])
|
| 80 |
+
|
| 81 |
+
return characters
|
| 82 |
+
else:
|
| 83 |
+
return [("", text)]
|
| 84 |
+
|
| 85 |
+
def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]:
|
| 86 |
+
lines = list(split_lines(self._get_text_fragments(document.text)))
|
| 87 |
+
|
| 88 |
+
def get_line(lineno: int) -> StyleAndTextTuples:
|
| 89 |
+
try:
|
| 90 |
+
return lines[lineno]
|
| 91 |
+
except IndexError:
|
| 92 |
+
return []
|
| 93 |
+
|
| 94 |
+
return get_line
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/regex_parser.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Parser for parsing a regular expression.
|
| 3 |
+
Take a string representing a regular expression and return the root node of its
|
| 4 |
+
parse tree.
|
| 5 |
+
|
| 6 |
+
usage::
|
| 7 |
+
|
| 8 |
+
root_node = parse_regex('(hello|world)')
|
| 9 |
+
|
| 10 |
+
Remarks:
|
| 11 |
+
- The regex parser processes multiline, it ignores all whitespace and supports
|
| 12 |
+
multiple named groups with the same name and #-style comments.
|
| 13 |
+
|
| 14 |
+
Limitations:
|
| 15 |
+
- Lookahead is not supported.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import re
|
| 21 |
+
|
| 22 |
+
__all__ = [
|
| 23 |
+
"Repeat",
|
| 24 |
+
"Variable",
|
| 25 |
+
"Regex",
|
| 26 |
+
"Lookahead",
|
| 27 |
+
"tokenize_regex",
|
| 28 |
+
"parse_regex",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class Node:
|
| 33 |
+
"""
|
| 34 |
+
Base class for all the grammar nodes.
|
| 35 |
+
(You don't initialize this one.)
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def __add__(self, other_node: Node) -> NodeSequence:
|
| 39 |
+
return NodeSequence([self, other_node])
|
| 40 |
+
|
| 41 |
+
def __or__(self, other_node: Node) -> AnyNode:
|
| 42 |
+
return AnyNode([self, other_node])
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class AnyNode(Node):
|
| 46 |
+
"""
|
| 47 |
+
Union operation (OR operation) between several grammars. You don't
|
| 48 |
+
initialize this yourself, but it's a result of a "Grammar1 | Grammar2"
|
| 49 |
+
operation.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(self, children: list[Node]) -> None:
|
| 53 |
+
self.children = children
|
| 54 |
+
|
| 55 |
+
def __or__(self, other_node: Node) -> AnyNode:
|
| 56 |
+
return AnyNode(self.children + [other_node])
|
| 57 |
+
|
| 58 |
+
def __repr__(self) -> str:
|
| 59 |
+
return f"{self.__class__.__name__}({self.children!r})"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class NodeSequence(Node):
|
| 63 |
+
"""
|
| 64 |
+
Concatenation operation of several grammars. You don't initialize this
|
| 65 |
+
yourself, but it's a result of a "Grammar1 + Grammar2" operation.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
def __init__(self, children: list[Node]) -> None:
|
| 69 |
+
self.children = children
|
| 70 |
+
|
| 71 |
+
def __add__(self, other_node: Node) -> NodeSequence:
|
| 72 |
+
return NodeSequence(self.children + [other_node])
|
| 73 |
+
|
| 74 |
+
def __repr__(self) -> str:
|
| 75 |
+
return f"{self.__class__.__name__}({self.children!r})"
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class Regex(Node):
|
| 79 |
+
"""
|
| 80 |
+
Regular expression.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
def __init__(self, regex: str) -> None:
|
| 84 |
+
re.compile(regex) # Validate
|
| 85 |
+
|
| 86 |
+
self.regex = regex
|
| 87 |
+
|
| 88 |
+
def __repr__(self) -> str:
|
| 89 |
+
return f"{self.__class__.__name__}(/{self.regex}/)"
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class Lookahead(Node):
|
| 93 |
+
"""
|
| 94 |
+
Lookahead expression.
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
def __init__(self, childnode: Node, negative: bool = False) -> None:
|
| 98 |
+
self.childnode = childnode
|
| 99 |
+
self.negative = negative
|
| 100 |
+
|
| 101 |
+
def __repr__(self) -> str:
|
| 102 |
+
return f"{self.__class__.__name__}({self.childnode!r})"
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class Variable(Node):
|
| 106 |
+
"""
|
| 107 |
+
Mark a variable in the regular grammar. This will be translated into a
|
| 108 |
+
named group. Each variable can have his own completer, validator, etc..
|
| 109 |
+
|
| 110 |
+
:param childnode: The grammar which is wrapped inside this variable.
|
| 111 |
+
:param varname: String.
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
def __init__(self, childnode: Node, varname: str = "") -> None:
|
| 115 |
+
self.childnode = childnode
|
| 116 |
+
self.varname = varname
|
| 117 |
+
|
| 118 |
+
def __repr__(self) -> str:
|
| 119 |
+
return f"{self.__class__.__name__}(childnode={self.childnode!r}, varname={self.varname!r})"
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class Repeat(Node):
|
| 123 |
+
def __init__(
|
| 124 |
+
self,
|
| 125 |
+
childnode: Node,
|
| 126 |
+
min_repeat: int = 0,
|
| 127 |
+
max_repeat: int | None = None,
|
| 128 |
+
greedy: bool = True,
|
| 129 |
+
) -> None:
|
| 130 |
+
self.childnode = childnode
|
| 131 |
+
self.min_repeat = min_repeat
|
| 132 |
+
self.max_repeat = max_repeat
|
| 133 |
+
self.greedy = greedy
|
| 134 |
+
|
| 135 |
+
def __repr__(self) -> str:
|
| 136 |
+
return f"{self.__class__.__name__}(childnode={self.childnode!r})"
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def tokenize_regex(input: str) -> list[str]:
|
| 140 |
+
"""
|
| 141 |
+
Takes a string, representing a regular expression as input, and tokenizes
|
| 142 |
+
it.
|
| 143 |
+
|
| 144 |
+
:param input: string, representing a regular expression.
|
| 145 |
+
:returns: List of tokens.
|
| 146 |
+
"""
|
| 147 |
+
# Regular expression for tokenizing other regular expressions.
|
| 148 |
+
p = re.compile(
|
| 149 |
+
r"""^(
|
| 150 |
+
\(\?P\<[a-zA-Z0-9_-]+\> | # Start of named group.
|
| 151 |
+
\(\?#[^)]*\) | # Comment
|
| 152 |
+
\(\?= | # Start of lookahead assertion
|
| 153 |
+
\(\?! | # Start of negative lookahead assertion
|
| 154 |
+
\(\?<= | # If preceded by.
|
| 155 |
+
\(\?< | # If not preceded by.
|
| 156 |
+
\(?: | # Start of group. (non capturing.)
|
| 157 |
+
\( | # Start of group.
|
| 158 |
+
\(?[iLmsux] | # Flags.
|
| 159 |
+
\(?P=[a-zA-Z]+\) | # Back reference to named group
|
| 160 |
+
\) | # End of group.
|
| 161 |
+
\{[^{}]*\} | # Repetition
|
| 162 |
+
\*\? | \+\? | \?\?\ | # Non greedy repetition.
|
| 163 |
+
\* | \+ | \? | # Repetition
|
| 164 |
+
\#.*\n | # Comment
|
| 165 |
+
\\. |
|
| 166 |
+
|
| 167 |
+
# Character group.
|
| 168 |
+
\[
|
| 169 |
+
( [^\]\\] | \\.)*
|
| 170 |
+
\] |
|
| 171 |
+
|
| 172 |
+
[^(){}] |
|
| 173 |
+
.
|
| 174 |
+
)""",
|
| 175 |
+
re.VERBOSE,
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
tokens = []
|
| 179 |
+
|
| 180 |
+
while input:
|
| 181 |
+
m = p.match(input)
|
| 182 |
+
if m:
|
| 183 |
+
token, input = input[: m.end()], input[m.end() :]
|
| 184 |
+
if not token.isspace():
|
| 185 |
+
tokens.append(token)
|
| 186 |
+
else:
|
| 187 |
+
raise Exception("Could not tokenize input regex.")
|
| 188 |
+
|
| 189 |
+
return tokens
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def parse_regex(regex_tokens: list[str]) -> Node:
|
| 193 |
+
"""
|
| 194 |
+
Takes a list of tokens from the tokenizer, and returns a parse tree.
|
| 195 |
+
"""
|
| 196 |
+
# We add a closing brace because that represents the final pop of the stack.
|
| 197 |
+
tokens: list[str] = [")"] + regex_tokens[::-1]
|
| 198 |
+
|
| 199 |
+
def wrap(lst: list[Node]) -> Node:
|
| 200 |
+
"""Turn list into sequence when it contains several items."""
|
| 201 |
+
if len(lst) == 1:
|
| 202 |
+
return lst[0]
|
| 203 |
+
else:
|
| 204 |
+
return NodeSequence(lst)
|
| 205 |
+
|
| 206 |
+
def _parse() -> Node:
|
| 207 |
+
or_list: list[list[Node]] = []
|
| 208 |
+
result: list[Node] = []
|
| 209 |
+
|
| 210 |
+
def wrapped_result() -> Node:
|
| 211 |
+
if or_list == []:
|
| 212 |
+
return wrap(result)
|
| 213 |
+
else:
|
| 214 |
+
or_list.append(result)
|
| 215 |
+
return AnyNode([wrap(i) for i in or_list])
|
| 216 |
+
|
| 217 |
+
while tokens:
|
| 218 |
+
t = tokens.pop()
|
| 219 |
+
|
| 220 |
+
if t.startswith("(?P<"):
|
| 221 |
+
variable = Variable(_parse(), varname=t[4:-1])
|
| 222 |
+
result.append(variable)
|
| 223 |
+
|
| 224 |
+
elif t in ("*", "*?"):
|
| 225 |
+
greedy = t == "*"
|
| 226 |
+
result[-1] = Repeat(result[-1], greedy=greedy)
|
| 227 |
+
|
| 228 |
+
elif t in ("+", "+?"):
|
| 229 |
+
greedy = t == "+"
|
| 230 |
+
result[-1] = Repeat(result[-1], min_repeat=1, greedy=greedy)
|
| 231 |
+
|
| 232 |
+
elif t in ("?", "??"):
|
| 233 |
+
if result == []:
|
| 234 |
+
raise Exception("Nothing to repeat." + repr(tokens))
|
| 235 |
+
else:
|
| 236 |
+
greedy = t == "?"
|
| 237 |
+
result[-1] = Repeat(
|
| 238 |
+
result[-1], min_repeat=0, max_repeat=1, greedy=greedy
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
elif t == "|":
|
| 242 |
+
or_list.append(result)
|
| 243 |
+
result = []
|
| 244 |
+
|
| 245 |
+
elif t in ("(", "(?:"):
|
| 246 |
+
result.append(_parse())
|
| 247 |
+
|
| 248 |
+
elif t == "(?!":
|
| 249 |
+
result.append(Lookahead(_parse(), negative=True))
|
| 250 |
+
|
| 251 |
+
elif t == "(?=":
|
| 252 |
+
result.append(Lookahead(_parse(), negative=False))
|
| 253 |
+
|
| 254 |
+
elif t == ")":
|
| 255 |
+
return wrapped_result()
|
| 256 |
+
|
| 257 |
+
elif t.startswith("#"):
|
| 258 |
+
pass
|
| 259 |
+
|
| 260 |
+
elif t.startswith("{"):
|
| 261 |
+
# TODO: implement!
|
| 262 |
+
raise Exception(f"{t}-style repetition not yet supported")
|
| 263 |
+
|
| 264 |
+
elif t.startswith("(?"):
|
| 265 |
+
raise Exception(f"{t!r} not supported")
|
| 266 |
+
|
| 267 |
+
elif t.isspace():
|
| 268 |
+
pass
|
| 269 |
+
else:
|
| 270 |
+
result.append(Regex(t))
|
| 271 |
+
|
| 272 |
+
raise Exception("Expecting ')' token")
|
| 273 |
+
|
| 274 |
+
result = _parse()
|
| 275 |
+
|
| 276 |
+
if len(tokens) != 0:
|
| 277 |
+
raise Exception("Unmatched parentheses.")
|
| 278 |
+
else:
|
| 279 |
+
return result
|
lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/validation.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Validator for a regular language.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from prompt_toolkit.document import Document
|
| 8 |
+
from prompt_toolkit.validation import ValidationError, Validator
|
| 9 |
+
|
| 10 |
+
from .compiler import _CompiledGrammar
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"GrammarValidator",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class GrammarValidator(Validator):
|
| 18 |
+
"""
|
| 19 |
+
Validator which can be used for validation according to variables in
|
| 20 |
+
the grammar. Each variable can have its own validator.
|
| 21 |
+
|
| 22 |
+
:param compiled_grammar: `GrammarCompleter` instance.
|
| 23 |
+
:param validators: `dict` mapping variable names of the grammar to the
|
| 24 |
+
`Validator` instances to be used for each variable.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(
|
| 28 |
+
self, compiled_grammar: _CompiledGrammar, validators: dict[str, Validator]
|
| 29 |
+
) -> None:
|
| 30 |
+
self.compiled_grammar = compiled_grammar
|
| 31 |
+
self.validators = validators
|
| 32 |
+
|
| 33 |
+
def validate(self, document: Document) -> None:
|
| 34 |
+
# Parse input document.
|
| 35 |
+
# We use `match`, not `match_prefix`, because for validation, we want
|
| 36 |
+
# the actual, unambiguous interpretation of the input.
|
| 37 |
+
m = self.compiled_grammar.match(document.text)
|
| 38 |
+
|
| 39 |
+
if m:
|
| 40 |
+
for v in m.variables():
|
| 41 |
+
validator = self.validators.get(v.varname)
|
| 42 |
+
|
| 43 |
+
if validator:
|
| 44 |
+
# Unescape text.
|
| 45 |
+
unwrapped_text = self.compiled_grammar.unescape(v.varname, v.value)
|
| 46 |
+
|
| 47 |
+
# Create a document, for the completions API (text/cursor_position)
|
| 48 |
+
inner_document = Document(unwrapped_text, len(unwrapped_text))
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
validator.validate(inner_document)
|
| 52 |
+
except ValidationError as e:
|
| 53 |
+
raise ValidationError(
|
| 54 |
+
cursor_position=v.start + e.cursor_position,
|
| 55 |
+
message=e.message,
|
| 56 |
+
) from e
|
| 57 |
+
else:
|
| 58 |
+
raise ValidationError(
|
| 59 |
+
cursor_position=len(document.text), message="Invalid command"
|
| 60 |
+
)
|
lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .server import PromptToolkitSSHServer, PromptToolkitSSHSession
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"PromptToolkitSSHSession",
|
| 7 |
+
"PromptToolkitSSHServer",
|
| 8 |
+
]
|
lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (389 Bytes). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/__pycache__/server.cpython-312.pyc
ADDED
|
Binary file (9.17 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/server.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility for running a prompt_toolkit application in an asyncssh server.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import traceback
|
| 9 |
+
from asyncio import get_running_loop
|
| 10 |
+
from typing import Any, Callable, Coroutine, TextIO, cast
|
| 11 |
+
|
| 12 |
+
import asyncssh
|
| 13 |
+
|
| 14 |
+
from prompt_toolkit.application.current import AppSession, create_app_session
|
| 15 |
+
from prompt_toolkit.data_structures import Size
|
| 16 |
+
from prompt_toolkit.input import PipeInput, create_pipe_input
|
| 17 |
+
from prompt_toolkit.output.vt100 import Vt100_Output
|
| 18 |
+
|
| 19 |
+
__all__ = ["PromptToolkitSSHSession", "PromptToolkitSSHServer"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class PromptToolkitSSHSession(asyncssh.SSHServerSession): # type: ignore
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
interact: Callable[[PromptToolkitSSHSession], Coroutine[Any, Any, None]],
|
| 26 |
+
*,
|
| 27 |
+
enable_cpr: bool,
|
| 28 |
+
) -> None:
|
| 29 |
+
self.interact = interact
|
| 30 |
+
self.enable_cpr = enable_cpr
|
| 31 |
+
self.interact_task: asyncio.Task[None] | None = None
|
| 32 |
+
self._chan: Any | None = None
|
| 33 |
+
self.app_session: AppSession | None = None
|
| 34 |
+
|
| 35 |
+
# PipInput object, for sending input in the CLI.
|
| 36 |
+
# (This is something that we can use in the prompt_toolkit event loop,
|
| 37 |
+
# but still write date in manually.)
|
| 38 |
+
self._input: PipeInput | None = None
|
| 39 |
+
self._output: Vt100_Output | None = None
|
| 40 |
+
|
| 41 |
+
# Output object. Don't render to the real stdout, but write everything
|
| 42 |
+
# in the SSH channel.
|
| 43 |
+
class Stdout:
|
| 44 |
+
def write(s, data: str) -> None:
|
| 45 |
+
try:
|
| 46 |
+
if self._chan is not None:
|
| 47 |
+
self._chan.write(data.replace("\n", "\r\n"))
|
| 48 |
+
except BrokenPipeError:
|
| 49 |
+
pass # Channel not open for sending.
|
| 50 |
+
|
| 51 |
+
def isatty(s) -> bool:
|
| 52 |
+
return True
|
| 53 |
+
|
| 54 |
+
def flush(s) -> None:
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def encoding(s) -> str:
|
| 59 |
+
assert self._chan is not None
|
| 60 |
+
return str(self._chan._orig_chan.get_encoding()[0])
|
| 61 |
+
|
| 62 |
+
self.stdout = cast(TextIO, Stdout())
|
| 63 |
+
|
| 64 |
+
def _get_size(self) -> Size:
|
| 65 |
+
"""
|
| 66 |
+
Callable that returns the current `Size`, required by Vt100_Output.
|
| 67 |
+
"""
|
| 68 |
+
if self._chan is None:
|
| 69 |
+
return Size(rows=20, columns=79)
|
| 70 |
+
else:
|
| 71 |
+
width, height, pixwidth, pixheight = self._chan.get_terminal_size()
|
| 72 |
+
return Size(rows=height, columns=width)
|
| 73 |
+
|
| 74 |
+
def connection_made(self, chan: Any) -> None:
|
| 75 |
+
self._chan = chan
|
| 76 |
+
|
| 77 |
+
def shell_requested(self) -> bool:
|
| 78 |
+
return True
|
| 79 |
+
|
| 80 |
+
def session_started(self) -> None:
|
| 81 |
+
self.interact_task = get_running_loop().create_task(self._interact())
|
| 82 |
+
|
| 83 |
+
async def _interact(self) -> None:
|
| 84 |
+
if self._chan is None:
|
| 85 |
+
# Should not happen.
|
| 86 |
+
raise Exception("`_interact` called before `connection_made`.")
|
| 87 |
+
|
| 88 |
+
if hasattr(self._chan, "set_line_mode") and self._chan._editor is not None:
|
| 89 |
+
# Disable the line editing provided by asyncssh. Prompt_toolkit
|
| 90 |
+
# provides the line editing.
|
| 91 |
+
self._chan.set_line_mode(False)
|
| 92 |
+
|
| 93 |
+
term = self._chan.get_terminal_type()
|
| 94 |
+
|
| 95 |
+
self._output = Vt100_Output(
|
| 96 |
+
self.stdout, self._get_size, term=term, enable_cpr=self.enable_cpr
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
with create_pipe_input() as self._input:
|
| 100 |
+
with create_app_session(input=self._input, output=self._output) as session:
|
| 101 |
+
self.app_session = session
|
| 102 |
+
try:
|
| 103 |
+
await self.interact(self)
|
| 104 |
+
except BaseException:
|
| 105 |
+
traceback.print_exc()
|
| 106 |
+
finally:
|
| 107 |
+
# Close the connection.
|
| 108 |
+
self._chan.close()
|
| 109 |
+
self._input.close()
|
| 110 |
+
|
| 111 |
+
def terminal_size_changed(
|
| 112 |
+
self, width: int, height: int, pixwidth: object, pixheight: object
|
| 113 |
+
) -> None:
|
| 114 |
+
# Send resize event to the current application.
|
| 115 |
+
if self.app_session and self.app_session.app:
|
| 116 |
+
self.app_session.app._on_resize()
|
| 117 |
+
|
| 118 |
+
def data_received(self, data: str, datatype: object) -> None:
|
| 119 |
+
if self._input is None:
|
| 120 |
+
# Should not happen.
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
self._input.send_text(data)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class PromptToolkitSSHServer(asyncssh.SSHServer):
|
| 127 |
+
"""
|
| 128 |
+
Run a prompt_toolkit application over an asyncssh server.
|
| 129 |
+
|
| 130 |
+
This takes one argument, an `interact` function, which is called for each
|
| 131 |
+
connection. This should be an asynchronous function that runs the
|
| 132 |
+
prompt_toolkit applications. This function runs in an `AppSession`, which
|
| 133 |
+
means that we can have multiple UI interactions concurrently.
|
| 134 |
+
|
| 135 |
+
Example usage:
|
| 136 |
+
|
| 137 |
+
.. code:: python
|
| 138 |
+
|
| 139 |
+
async def interact(ssh_session: PromptToolkitSSHSession) -> None:
|
| 140 |
+
await yes_no_dialog("my title", "my text").run_async()
|
| 141 |
+
|
| 142 |
+
prompt_session = PromptSession()
|
| 143 |
+
text = await prompt_session.prompt_async("Type something: ")
|
| 144 |
+
print_formatted_text('You said: ', text)
|
| 145 |
+
|
| 146 |
+
server = PromptToolkitSSHServer(interact=interact)
|
| 147 |
+
loop = get_running_loop()
|
| 148 |
+
loop.run_until_complete(
|
| 149 |
+
asyncssh.create_server(
|
| 150 |
+
lambda: MySSHServer(interact),
|
| 151 |
+
"",
|
| 152 |
+
port,
|
| 153 |
+
server_host_keys=["/etc/ssh/..."],
|
| 154 |
+
)
|
| 155 |
+
)
|
| 156 |
+
loop.run_forever()
|
| 157 |
+
|
| 158 |
+
:param enable_cpr: When `True`, the default, try to detect whether the SSH
|
| 159 |
+
client runs in a terminal that responds to "cursor position requests".
|
| 160 |
+
That way, we can properly determine how much space there is available
|
| 161 |
+
for the UI (especially for drop down menus) to render.
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
def __init__(
|
| 165 |
+
self,
|
| 166 |
+
interact: Callable[[PromptToolkitSSHSession], Coroutine[Any, Any, None]],
|
| 167 |
+
*,
|
| 168 |
+
enable_cpr: bool = True,
|
| 169 |
+
) -> None:
|
| 170 |
+
self.interact = interact
|
| 171 |
+
self.enable_cpr = enable_cpr
|
| 172 |
+
|
| 173 |
+
def begin_auth(self, username: str) -> bool:
|
| 174 |
+
# No authentication.
|
| 175 |
+
return False
|
| 176 |
+
|
| 177 |
+
def session_requested(self) -> PromptToolkitSSHSession:
|
| 178 |
+
return PromptToolkitSSHSession(self.interact, enable_cpr=self.enable_cpr)
|
lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .server import TelnetServer
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"TelnetServer",
|
| 7 |
+
]
|
lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (338 Bytes). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/log.cpython-312.pyc
ADDED
|
Binary file (450 Bytes). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/protocol.cpython-312.pyc
ADDED
|
Binary file (7.66 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/server.cpython-312.pyc
ADDED
|
Binary file (20.3 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/log.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Python logger for the telnet server.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__package__)
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"logger",
|
| 13 |
+
]
|
lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/protocol.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Parser for the Telnet protocol. (Not a complete implementation of the telnet
|
| 3 |
+
specification, but sufficient for a command line interface.)
|
| 4 |
+
|
| 5 |
+
Inspired by `Twisted.conch.telnet`.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import struct
|
| 11 |
+
from typing import Callable, Generator
|
| 12 |
+
|
| 13 |
+
from .log import logger
|
| 14 |
+
|
| 15 |
+
__all__ = [
|
| 16 |
+
"TelnetProtocolParser",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def int2byte(number: int) -> bytes:
|
| 21 |
+
return bytes((number,))
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Telnet constants.
|
| 25 |
+
NOP = int2byte(0)
|
| 26 |
+
SGA = int2byte(3)
|
| 27 |
+
|
| 28 |
+
IAC = int2byte(255)
|
| 29 |
+
DO = int2byte(253)
|
| 30 |
+
DONT = int2byte(254)
|
| 31 |
+
LINEMODE = int2byte(34)
|
| 32 |
+
SB = int2byte(250)
|
| 33 |
+
WILL = int2byte(251)
|
| 34 |
+
WONT = int2byte(252)
|
| 35 |
+
MODE = int2byte(1)
|
| 36 |
+
SE = int2byte(240)
|
| 37 |
+
ECHO = int2byte(1)
|
| 38 |
+
NAWS = int2byte(31)
|
| 39 |
+
LINEMODE = int2byte(34)
|
| 40 |
+
SUPPRESS_GO_AHEAD = int2byte(3)
|
| 41 |
+
|
| 42 |
+
TTYPE = int2byte(24)
|
| 43 |
+
SEND = int2byte(1)
|
| 44 |
+
IS = int2byte(0)
|
| 45 |
+
|
| 46 |
+
DM = int2byte(242)
|
| 47 |
+
BRK = int2byte(243)
|
| 48 |
+
IP = int2byte(244)
|
| 49 |
+
AO = int2byte(245)
|
| 50 |
+
AYT = int2byte(246)
|
| 51 |
+
EC = int2byte(247)
|
| 52 |
+
EL = int2byte(248)
|
| 53 |
+
GA = int2byte(249)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class TelnetProtocolParser:
|
| 57 |
+
"""
|
| 58 |
+
Parser for the Telnet protocol.
|
| 59 |
+
Usage::
|
| 60 |
+
|
| 61 |
+
def data_received(data):
|
| 62 |
+
print(data)
|
| 63 |
+
|
| 64 |
+
def size_received(rows, columns):
|
| 65 |
+
print(rows, columns)
|
| 66 |
+
|
| 67 |
+
p = TelnetProtocolParser(data_received, size_received)
|
| 68 |
+
p.feed(binary_data)
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
def __init__(
|
| 72 |
+
self,
|
| 73 |
+
data_received_callback: Callable[[bytes], None],
|
| 74 |
+
size_received_callback: Callable[[int, int], None],
|
| 75 |
+
ttype_received_callback: Callable[[str], None],
|
| 76 |
+
) -> None:
|
| 77 |
+
self.data_received_callback = data_received_callback
|
| 78 |
+
self.size_received_callback = size_received_callback
|
| 79 |
+
self.ttype_received_callback = ttype_received_callback
|
| 80 |
+
|
| 81 |
+
self._parser = self._parse_coroutine()
|
| 82 |
+
self._parser.send(None) # type: ignore
|
| 83 |
+
|
| 84 |
+
def received_data(self, data: bytes) -> None:
|
| 85 |
+
self.data_received_callback(data)
|
| 86 |
+
|
| 87 |
+
def do_received(self, data: bytes) -> None:
|
| 88 |
+
"""Received telnet DO command."""
|
| 89 |
+
logger.info("DO %r", data)
|
| 90 |
+
|
| 91 |
+
def dont_received(self, data: bytes) -> None:
|
| 92 |
+
"""Received telnet DONT command."""
|
| 93 |
+
logger.info("DONT %r", data)
|
| 94 |
+
|
| 95 |
+
def will_received(self, data: bytes) -> None:
|
| 96 |
+
"""Received telnet WILL command."""
|
| 97 |
+
logger.info("WILL %r", data)
|
| 98 |
+
|
| 99 |
+
def wont_received(self, data: bytes) -> None:
|
| 100 |
+
"""Received telnet WONT command."""
|
| 101 |
+
logger.info("WONT %r", data)
|
| 102 |
+
|
| 103 |
+
def command_received(self, command: bytes, data: bytes) -> None:
|
| 104 |
+
if command == DO:
|
| 105 |
+
self.do_received(data)
|
| 106 |
+
|
| 107 |
+
elif command == DONT:
|
| 108 |
+
self.dont_received(data)
|
| 109 |
+
|
| 110 |
+
elif command == WILL:
|
| 111 |
+
self.will_received(data)
|
| 112 |
+
|
| 113 |
+
elif command == WONT:
|
| 114 |
+
self.wont_received(data)
|
| 115 |
+
|
| 116 |
+
else:
|
| 117 |
+
logger.info("command received %r %r", command, data)
|
| 118 |
+
|
| 119 |
+
def naws(self, data: bytes) -> None:
|
| 120 |
+
"""
|
| 121 |
+
Received NAWS. (Window dimensions.)
|
| 122 |
+
"""
|
| 123 |
+
if len(data) == 4:
|
| 124 |
+
# NOTE: the first parameter of struct.unpack should be
|
| 125 |
+
# a 'str' object. Both on Py2/py3. This crashes on OSX
|
| 126 |
+
# otherwise.
|
| 127 |
+
columns, rows = struct.unpack("!HH", data)
|
| 128 |
+
self.size_received_callback(rows, columns)
|
| 129 |
+
else:
|
| 130 |
+
logger.warning("Wrong number of NAWS bytes")
|
| 131 |
+
|
| 132 |
+
def ttype(self, data: bytes) -> None:
|
| 133 |
+
"""
|
| 134 |
+
Received terminal type.
|
| 135 |
+
"""
|
| 136 |
+
subcmd, data = data[0:1], data[1:]
|
| 137 |
+
if subcmd == IS:
|
| 138 |
+
ttype = data.decode("ascii")
|
| 139 |
+
self.ttype_received_callback(ttype)
|
| 140 |
+
else:
|
| 141 |
+
logger.warning("Received a non-IS terminal type Subnegotiation")
|
| 142 |
+
|
| 143 |
+
def negotiate(self, data: bytes) -> None:
|
| 144 |
+
"""
|
| 145 |
+
Got negotiate data.
|
| 146 |
+
"""
|
| 147 |
+
command, payload = data[0:1], data[1:]
|
| 148 |
+
|
| 149 |
+
if command == NAWS:
|
| 150 |
+
self.naws(payload)
|
| 151 |
+
elif command == TTYPE:
|
| 152 |
+
self.ttype(payload)
|
| 153 |
+
else:
|
| 154 |
+
logger.info("Negotiate (%r got bytes)", len(data))
|
| 155 |
+
|
| 156 |
+
def _parse_coroutine(self) -> Generator[None, bytes, None]:
|
| 157 |
+
"""
|
| 158 |
+
Parser state machine.
|
| 159 |
+
Every 'yield' expression returns the next byte.
|
| 160 |
+
"""
|
| 161 |
+
while True:
|
| 162 |
+
d = yield
|
| 163 |
+
|
| 164 |
+
if d == int2byte(0):
|
| 165 |
+
pass # NOP
|
| 166 |
+
|
| 167 |
+
# Go to state escaped.
|
| 168 |
+
elif d == IAC:
|
| 169 |
+
d2 = yield
|
| 170 |
+
|
| 171 |
+
if d2 == IAC:
|
| 172 |
+
self.received_data(d2)
|
| 173 |
+
|
| 174 |
+
# Handle simple commands.
|
| 175 |
+
elif d2 in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA):
|
| 176 |
+
self.command_received(d2, b"")
|
| 177 |
+
|
| 178 |
+
# Handle IAC-[DO/DONT/WILL/WONT] commands.
|
| 179 |
+
elif d2 in (DO, DONT, WILL, WONT):
|
| 180 |
+
d3 = yield
|
| 181 |
+
self.command_received(d2, d3)
|
| 182 |
+
|
| 183 |
+
# Subnegotiation
|
| 184 |
+
elif d2 == SB:
|
| 185 |
+
# Consume everything until next IAC-SE
|
| 186 |
+
data = []
|
| 187 |
+
|
| 188 |
+
while True:
|
| 189 |
+
d3 = yield
|
| 190 |
+
|
| 191 |
+
if d3 == IAC:
|
| 192 |
+
d4 = yield
|
| 193 |
+
if d4 == SE:
|
| 194 |
+
break
|
| 195 |
+
else:
|
| 196 |
+
data.append(d4)
|
| 197 |
+
else:
|
| 198 |
+
data.append(d3)
|
| 199 |
+
|
| 200 |
+
self.negotiate(b"".join(data))
|
| 201 |
+
else:
|
| 202 |
+
self.received_data(d)
|
| 203 |
+
|
| 204 |
+
def feed(self, data: bytes) -> None:
|
| 205 |
+
"""
|
| 206 |
+
Feed data to the parser.
|
| 207 |
+
"""
|
| 208 |
+
for b in data:
|
| 209 |
+
self._parser.send(int2byte(b))
|
lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/server.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Telnet server.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import contextvars
|
| 9 |
+
import socket
|
| 10 |
+
from asyncio import get_running_loop
|
| 11 |
+
from typing import Any, Callable, Coroutine, TextIO, cast
|
| 12 |
+
|
| 13 |
+
from prompt_toolkit.application.current import create_app_session, get_app
|
| 14 |
+
from prompt_toolkit.application.run_in_terminal import run_in_terminal
|
| 15 |
+
from prompt_toolkit.data_structures import Size
|
| 16 |
+
from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text
|
| 17 |
+
from prompt_toolkit.input import PipeInput, create_pipe_input
|
| 18 |
+
from prompt_toolkit.output.vt100 import Vt100_Output
|
| 19 |
+
from prompt_toolkit.renderer import print_formatted_text as print_formatted_text
|
| 20 |
+
from prompt_toolkit.styles import BaseStyle, DummyStyle
|
| 21 |
+
|
| 22 |
+
from .log import logger
|
| 23 |
+
from .protocol import (
|
| 24 |
+
DO,
|
| 25 |
+
ECHO,
|
| 26 |
+
IAC,
|
| 27 |
+
LINEMODE,
|
| 28 |
+
MODE,
|
| 29 |
+
NAWS,
|
| 30 |
+
SB,
|
| 31 |
+
SE,
|
| 32 |
+
SEND,
|
| 33 |
+
SUPPRESS_GO_AHEAD,
|
| 34 |
+
TTYPE,
|
| 35 |
+
WILL,
|
| 36 |
+
TelnetProtocolParser,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
__all__ = [
|
| 40 |
+
"TelnetServer",
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def int2byte(number: int) -> bytes:
|
| 45 |
+
return bytes((number,))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _initialize_telnet(connection: socket.socket) -> None:
|
| 49 |
+
logger.info("Initializing telnet connection")
|
| 50 |
+
|
| 51 |
+
# Iac Do Linemode
|
| 52 |
+
connection.send(IAC + DO + LINEMODE)
|
| 53 |
+
|
| 54 |
+
# Suppress Go Ahead. (This seems important for Putty to do correct echoing.)
|
| 55 |
+
# This will allow bi-directional operation.
|
| 56 |
+
connection.send(IAC + WILL + SUPPRESS_GO_AHEAD)
|
| 57 |
+
|
| 58 |
+
# Iac sb
|
| 59 |
+
connection.send(IAC + SB + LINEMODE + MODE + int2byte(0) + IAC + SE)
|
| 60 |
+
|
| 61 |
+
# IAC Will Echo
|
| 62 |
+
connection.send(IAC + WILL + ECHO)
|
| 63 |
+
|
| 64 |
+
# Negotiate window size
|
| 65 |
+
connection.send(IAC + DO + NAWS)
|
| 66 |
+
|
| 67 |
+
# Negotiate terminal type
|
| 68 |
+
# Assume the client will accept the negotiation with `IAC + WILL + TTYPE`
|
| 69 |
+
connection.send(IAC + DO + TTYPE)
|
| 70 |
+
|
| 71 |
+
# We can then select the first terminal type supported by the client,
|
| 72 |
+
# which is generally the best type the client supports
|
| 73 |
+
# The client should reply with a `IAC + SB + TTYPE + IS + ttype + IAC + SE`
|
| 74 |
+
connection.send(IAC + SB + TTYPE + SEND + IAC + SE)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class _ConnectionStdout:
|
| 78 |
+
"""
|
| 79 |
+
Wrapper around socket which provides `write` and `flush` methods for the
|
| 80 |
+
Vt100_Output output.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
def __init__(self, connection: socket.socket, encoding: str) -> None:
|
| 84 |
+
self._encoding = encoding
|
| 85 |
+
self._connection = connection
|
| 86 |
+
self._errors = "strict"
|
| 87 |
+
self._buffer: list[bytes] = []
|
| 88 |
+
self._closed = False
|
| 89 |
+
|
| 90 |
+
def write(self, data: str) -> None:
|
| 91 |
+
data = data.replace("\n", "\r\n")
|
| 92 |
+
self._buffer.append(data.encode(self._encoding, errors=self._errors))
|
| 93 |
+
self.flush()
|
| 94 |
+
|
| 95 |
+
def isatty(self) -> bool:
|
| 96 |
+
return True
|
| 97 |
+
|
| 98 |
+
def flush(self) -> None:
|
| 99 |
+
try:
|
| 100 |
+
if not self._closed:
|
| 101 |
+
self._connection.send(b"".join(self._buffer))
|
| 102 |
+
except OSError as e:
|
| 103 |
+
logger.warning(f"Couldn't send data over socket: {e}")
|
| 104 |
+
|
| 105 |
+
self._buffer = []
|
| 106 |
+
|
| 107 |
+
def close(self) -> None:
|
| 108 |
+
self._closed = True
|
| 109 |
+
|
| 110 |
+
@property
|
| 111 |
+
def encoding(self) -> str:
|
| 112 |
+
return self._encoding
|
| 113 |
+
|
| 114 |
+
@property
|
| 115 |
+
def errors(self) -> str:
|
| 116 |
+
return self._errors
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class TelnetConnection:
|
| 120 |
+
"""
|
| 121 |
+
Class that represents one Telnet connection.
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
def __init__(
|
| 125 |
+
self,
|
| 126 |
+
conn: socket.socket,
|
| 127 |
+
addr: tuple[str, int],
|
| 128 |
+
interact: Callable[[TelnetConnection], Coroutine[Any, Any, None]],
|
| 129 |
+
server: TelnetServer,
|
| 130 |
+
encoding: str,
|
| 131 |
+
style: BaseStyle | None,
|
| 132 |
+
vt100_input: PipeInput,
|
| 133 |
+
enable_cpr: bool = True,
|
| 134 |
+
) -> None:
|
| 135 |
+
self.conn = conn
|
| 136 |
+
self.addr = addr
|
| 137 |
+
self.interact = interact
|
| 138 |
+
self.server = server
|
| 139 |
+
self.encoding = encoding
|
| 140 |
+
self.style = style
|
| 141 |
+
self._closed = False
|
| 142 |
+
self._ready = asyncio.Event()
|
| 143 |
+
self.vt100_input = vt100_input
|
| 144 |
+
self.enable_cpr = enable_cpr
|
| 145 |
+
self.vt100_output: Vt100_Output | None = None
|
| 146 |
+
|
| 147 |
+
# Create "Output" object.
|
| 148 |
+
self.size = Size(rows=40, columns=79)
|
| 149 |
+
|
| 150 |
+
# Initialize.
|
| 151 |
+
_initialize_telnet(conn)
|
| 152 |
+
|
| 153 |
+
# Create output.
|
| 154 |
+
def get_size() -> Size:
|
| 155 |
+
return self.size
|
| 156 |
+
|
| 157 |
+
self.stdout = cast(TextIO, _ConnectionStdout(conn, encoding=encoding))
|
| 158 |
+
|
| 159 |
+
def data_received(data: bytes) -> None:
|
| 160 |
+
"""TelnetProtocolParser 'data_received' callback"""
|
| 161 |
+
self.vt100_input.send_bytes(data)
|
| 162 |
+
|
| 163 |
+
def size_received(rows: int, columns: int) -> None:
|
| 164 |
+
"""TelnetProtocolParser 'size_received' callback"""
|
| 165 |
+
self.size = Size(rows=rows, columns=columns)
|
| 166 |
+
if self.vt100_output is not None and self.context:
|
| 167 |
+
self.context.run(lambda: get_app()._on_resize())
|
| 168 |
+
|
| 169 |
+
def ttype_received(ttype: str) -> None:
|
| 170 |
+
"""TelnetProtocolParser 'ttype_received' callback"""
|
| 171 |
+
self.vt100_output = Vt100_Output(
|
| 172 |
+
self.stdout, get_size, term=ttype, enable_cpr=enable_cpr
|
| 173 |
+
)
|
| 174 |
+
self._ready.set()
|
| 175 |
+
|
| 176 |
+
self.parser = TelnetProtocolParser(data_received, size_received, ttype_received)
|
| 177 |
+
self.context: contextvars.Context | None = None
|
| 178 |
+
|
| 179 |
+
async def run_application(self) -> None:
|
| 180 |
+
"""
|
| 181 |
+
Run application.
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
def handle_incoming_data() -> None:
|
| 185 |
+
data = self.conn.recv(1024)
|
| 186 |
+
if data:
|
| 187 |
+
self.feed(data)
|
| 188 |
+
else:
|
| 189 |
+
# Connection closed by client.
|
| 190 |
+
logger.info("Connection closed by client. {!r} {!r}".format(*self.addr))
|
| 191 |
+
self.close()
|
| 192 |
+
|
| 193 |
+
# Add reader.
|
| 194 |
+
loop = get_running_loop()
|
| 195 |
+
loop.add_reader(self.conn, handle_incoming_data)
|
| 196 |
+
|
| 197 |
+
try:
|
| 198 |
+
# Wait for v100_output to be properly instantiated
|
| 199 |
+
await self._ready.wait()
|
| 200 |
+
with create_app_session(input=self.vt100_input, output=self.vt100_output):
|
| 201 |
+
self.context = contextvars.copy_context()
|
| 202 |
+
await self.interact(self)
|
| 203 |
+
finally:
|
| 204 |
+
self.close()
|
| 205 |
+
|
| 206 |
+
def feed(self, data: bytes) -> None:
|
| 207 |
+
"""
|
| 208 |
+
Handler for incoming data. (Called by TelnetServer.)
|
| 209 |
+
"""
|
| 210 |
+
self.parser.feed(data)
|
| 211 |
+
|
| 212 |
+
def close(self) -> None:
|
| 213 |
+
"""
|
| 214 |
+
Closed by client.
|
| 215 |
+
"""
|
| 216 |
+
if not self._closed:
|
| 217 |
+
self._closed = True
|
| 218 |
+
|
| 219 |
+
self.vt100_input.close()
|
| 220 |
+
get_running_loop().remove_reader(self.conn)
|
| 221 |
+
self.conn.close()
|
| 222 |
+
self.stdout.close()
|
| 223 |
+
|
| 224 |
+
def send(self, formatted_text: AnyFormattedText) -> None:
|
| 225 |
+
"""
|
| 226 |
+
Send text to the client.
|
| 227 |
+
"""
|
| 228 |
+
if self.vt100_output is None:
|
| 229 |
+
return
|
| 230 |
+
formatted_text = to_formatted_text(formatted_text)
|
| 231 |
+
print_formatted_text(
|
| 232 |
+
self.vt100_output, formatted_text, self.style or DummyStyle()
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
def send_above_prompt(self, formatted_text: AnyFormattedText) -> None:
|
| 236 |
+
"""
|
| 237 |
+
Send text to the client.
|
| 238 |
+
This is asynchronous, returns a `Future`.
|
| 239 |
+
"""
|
| 240 |
+
formatted_text = to_formatted_text(formatted_text)
|
| 241 |
+
return self._run_in_terminal(lambda: self.send(formatted_text))
|
| 242 |
+
|
| 243 |
+
def _run_in_terminal(self, func: Callable[[], None]) -> None:
|
| 244 |
+
# Make sure that when an application was active for this connection,
|
| 245 |
+
# that we print the text above the application.
|
| 246 |
+
if self.context:
|
| 247 |
+
self.context.run(run_in_terminal, func)
|
| 248 |
+
else:
|
| 249 |
+
raise RuntimeError("Called _run_in_terminal outside `run_application`.")
|
| 250 |
+
|
| 251 |
+
def erase_screen(self) -> None:
|
| 252 |
+
"""
|
| 253 |
+
Erase the screen and move the cursor to the top.
|
| 254 |
+
"""
|
| 255 |
+
if self.vt100_output is None:
|
| 256 |
+
return
|
| 257 |
+
self.vt100_output.erase_screen()
|
| 258 |
+
self.vt100_output.cursor_goto(0, 0)
|
| 259 |
+
self.vt100_output.flush()
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
async def _dummy_interact(connection: TelnetConnection) -> None:
|
| 263 |
+
pass
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
class TelnetServer:
|
| 267 |
+
"""
|
| 268 |
+
Telnet server implementation.
|
| 269 |
+
|
| 270 |
+
Example::
|
| 271 |
+
|
| 272 |
+
async def interact(connection):
|
| 273 |
+
connection.send("Welcome")
|
| 274 |
+
session = PromptSession()
|
| 275 |
+
result = await session.prompt_async(message="Say something: ")
|
| 276 |
+
connection.send(f"You said: {result}\n")
|
| 277 |
+
|
| 278 |
+
async def main():
|
| 279 |
+
server = TelnetServer(interact=interact, port=2323)
|
| 280 |
+
await server.run()
|
| 281 |
+
"""
|
| 282 |
+
|
| 283 |
+
def __init__(
|
| 284 |
+
self,
|
| 285 |
+
host: str = "127.0.0.1",
|
| 286 |
+
port: int = 23,
|
| 287 |
+
interact: Callable[
|
| 288 |
+
[TelnetConnection], Coroutine[Any, Any, None]
|
| 289 |
+
] = _dummy_interact,
|
| 290 |
+
encoding: str = "utf-8",
|
| 291 |
+
style: BaseStyle | None = None,
|
| 292 |
+
enable_cpr: bool = True,
|
| 293 |
+
) -> None:
|
| 294 |
+
self.host = host
|
| 295 |
+
self.port = port
|
| 296 |
+
self.interact = interact
|
| 297 |
+
self.encoding = encoding
|
| 298 |
+
self.style = style
|
| 299 |
+
self.enable_cpr = enable_cpr
|
| 300 |
+
|
| 301 |
+
self._run_task: asyncio.Task[None] | None = None
|
| 302 |
+
self._application_tasks: list[asyncio.Task[None]] = []
|
| 303 |
+
|
| 304 |
+
self.connections: set[TelnetConnection] = set()
|
| 305 |
+
|
| 306 |
+
@classmethod
|
| 307 |
+
def _create_socket(cls, host: str, port: int) -> socket.socket:
|
| 308 |
+
# Create and bind socket
|
| 309 |
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 310 |
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| 311 |
+
s.bind((host, port))
|
| 312 |
+
|
| 313 |
+
s.listen(4)
|
| 314 |
+
return s
|
| 315 |
+
|
| 316 |
+
async def run(self, ready_cb: Callable[[], None] | None = None) -> None:
|
| 317 |
+
"""
|
| 318 |
+
Run the telnet server, until this gets cancelled.
|
| 319 |
+
|
| 320 |
+
:param ready_cb: Callback that will be called at the point that we're
|
| 321 |
+
actually listening.
|
| 322 |
+
"""
|
| 323 |
+
socket = self._create_socket(self.host, self.port)
|
| 324 |
+
logger.info(
|
| 325 |
+
"Listening for telnet connections on %s port %r", self.host, self.port
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
get_running_loop().add_reader(socket, lambda: self._accept(socket))
|
| 329 |
+
|
| 330 |
+
if ready_cb:
|
| 331 |
+
ready_cb()
|
| 332 |
+
|
| 333 |
+
try:
|
| 334 |
+
# Run forever, until cancelled.
|
| 335 |
+
await asyncio.Future()
|
| 336 |
+
finally:
|
| 337 |
+
get_running_loop().remove_reader(socket)
|
| 338 |
+
socket.close()
|
| 339 |
+
|
| 340 |
+
# Wait for all applications to finish.
|
| 341 |
+
for t in self._application_tasks:
|
| 342 |
+
t.cancel()
|
| 343 |
+
|
| 344 |
+
# (This is similar to
|
| 345 |
+
# `Application.cancel_and_wait_for_background_tasks`. We wait for the
|
| 346 |
+
# background tasks to complete, but don't propagate exceptions, because
|
| 347 |
+
# we can't use `ExceptionGroup` yet.)
|
| 348 |
+
if len(self._application_tasks) > 0:
|
| 349 |
+
await asyncio.wait(
|
| 350 |
+
self._application_tasks,
|
| 351 |
+
timeout=None,
|
| 352 |
+
return_when=asyncio.ALL_COMPLETED,
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
def start(self) -> None:
|
| 356 |
+
"""
|
| 357 |
+
Deprecated: Use `.run()` instead.
|
| 358 |
+
|
| 359 |
+
Start the telnet server (stop by calling and awaiting `stop()`).
|
| 360 |
+
"""
|
| 361 |
+
if self._run_task is not None:
|
| 362 |
+
# Already running.
|
| 363 |
+
return
|
| 364 |
+
|
| 365 |
+
self._run_task = get_running_loop().create_task(self.run())
|
| 366 |
+
|
| 367 |
+
async def stop(self) -> None:
|
| 368 |
+
"""
|
| 369 |
+
Deprecated: Use `.run()` instead.
|
| 370 |
+
|
| 371 |
+
Stop a telnet server that was started using `.start()` and wait for the
|
| 372 |
+
cancellation to complete.
|
| 373 |
+
"""
|
| 374 |
+
if self._run_task is not None:
|
| 375 |
+
self._run_task.cancel()
|
| 376 |
+
try:
|
| 377 |
+
await self._run_task
|
| 378 |
+
except asyncio.CancelledError:
|
| 379 |
+
pass
|
| 380 |
+
|
| 381 |
+
def _accept(self, listen_socket: socket.socket) -> None:
|
| 382 |
+
"""
|
| 383 |
+
Accept new incoming connection.
|
| 384 |
+
"""
|
| 385 |
+
conn, addr = listen_socket.accept()
|
| 386 |
+
logger.info("New connection %r %r", *addr)
|
| 387 |
+
|
| 388 |
+
# Run application for this connection.
|
| 389 |
+
async def run() -> None:
|
| 390 |
+
try:
|
| 391 |
+
with create_pipe_input() as vt100_input:
|
| 392 |
+
connection = TelnetConnection(
|
| 393 |
+
conn,
|
| 394 |
+
addr,
|
| 395 |
+
self.interact,
|
| 396 |
+
self,
|
| 397 |
+
encoding=self.encoding,
|
| 398 |
+
style=self.style,
|
| 399 |
+
vt100_input=vt100_input,
|
| 400 |
+
enable_cpr=self.enable_cpr,
|
| 401 |
+
)
|
| 402 |
+
self.connections.add(connection)
|
| 403 |
+
|
| 404 |
+
logger.info("Starting interaction %r %r", *addr)
|
| 405 |
+
try:
|
| 406 |
+
await connection.run_application()
|
| 407 |
+
finally:
|
| 408 |
+
self.connections.remove(connection)
|
| 409 |
+
logger.info("Stopping interaction %r %r", *addr)
|
| 410 |
+
except EOFError:
|
| 411 |
+
# Happens either when the connection is closed by the client
|
| 412 |
+
# (e.g., when the user types 'control-]', then 'quit' in the
|
| 413 |
+
# telnet client) or when the user types control-d in a prompt
|
| 414 |
+
# and this is not handled by the interact function.
|
| 415 |
+
logger.info("Unhandled EOFError in telnet application.")
|
| 416 |
+
except KeyboardInterrupt:
|
| 417 |
+
# Unhandled control-c propagated by a prompt.
|
| 418 |
+
logger.info("Unhandled KeyboardInterrupt in telnet application.")
|
| 419 |
+
except BaseException as e:
|
| 420 |
+
print(f"Got {type(e).__name__}", e)
|
| 421 |
+
import traceback
|
| 422 |
+
|
| 423 |
+
traceback.print_exc()
|
| 424 |
+
finally:
|
| 425 |
+
self._application_tasks.remove(task)
|
| 426 |
+
|
| 427 |
+
task = get_running_loop().create_task(run())
|
| 428 |
+
self._application_tasks.append(task)
|
lib/python3.12/site-packages/prompt_toolkit/cursor_shapes.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
from enum import Enum
|
| 5 |
+
from typing import TYPE_CHECKING, Any, Callable, Union
|
| 6 |
+
|
| 7 |
+
from prompt_toolkit.enums import EditingMode
|
| 8 |
+
from prompt_toolkit.key_binding.vi_state import InputMode
|
| 9 |
+
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
from .application import Application
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"CursorShape",
|
| 15 |
+
"CursorShapeConfig",
|
| 16 |
+
"SimpleCursorShapeConfig",
|
| 17 |
+
"ModalCursorShapeConfig",
|
| 18 |
+
"DynamicCursorShapeConfig",
|
| 19 |
+
"to_cursor_shape_config",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class CursorShape(Enum):
|
| 24 |
+
# Default value that should tell the output implementation to never send
|
| 25 |
+
# cursor shape escape sequences. This is the default right now, because
|
| 26 |
+
# before this `CursorShape` functionality was introduced into
|
| 27 |
+
# prompt_toolkit itself, people had workarounds to send cursor shapes
|
| 28 |
+
# escapes into the terminal, by monkey patching some of prompt_toolkit's
|
| 29 |
+
# internals. We don't want the default prompt_toolkit implementation to
|
| 30 |
+
# interfere with that. E.g., IPython patches the `ViState.input_mode`
|
| 31 |
+
# property. See: https://github.com/ipython/ipython/pull/13501/files
|
| 32 |
+
_NEVER_CHANGE = "_NEVER_CHANGE"
|
| 33 |
+
|
| 34 |
+
BLOCK = "BLOCK"
|
| 35 |
+
BEAM = "BEAM"
|
| 36 |
+
UNDERLINE = "UNDERLINE"
|
| 37 |
+
BLINKING_BLOCK = "BLINKING_BLOCK"
|
| 38 |
+
BLINKING_BEAM = "BLINKING_BEAM"
|
| 39 |
+
BLINKING_UNDERLINE = "BLINKING_UNDERLINE"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class CursorShapeConfig(ABC):
|
| 43 |
+
@abstractmethod
|
| 44 |
+
def get_cursor_shape(self, application: Application[Any]) -> CursorShape:
|
| 45 |
+
"""
|
| 46 |
+
Return the cursor shape to be used in the current state.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
AnyCursorShapeConfig = Union[CursorShape, CursorShapeConfig, None]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class SimpleCursorShapeConfig(CursorShapeConfig):
|
| 54 |
+
"""
|
| 55 |
+
Always show the given cursor shape.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
def __init__(self, cursor_shape: CursorShape = CursorShape._NEVER_CHANGE) -> None:
|
| 59 |
+
self.cursor_shape = cursor_shape
|
| 60 |
+
|
| 61 |
+
def get_cursor_shape(self, application: Application[Any]) -> CursorShape:
|
| 62 |
+
return self.cursor_shape
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class ModalCursorShapeConfig(CursorShapeConfig):
|
| 66 |
+
"""
|
| 67 |
+
Show cursor shape according to the current input mode.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def get_cursor_shape(self, application: Application[Any]) -> CursorShape:
|
| 71 |
+
if application.editing_mode == EditingMode.VI:
|
| 72 |
+
if application.vi_state.input_mode in {
|
| 73 |
+
InputMode.NAVIGATION,
|
| 74 |
+
}:
|
| 75 |
+
return CursorShape.BLOCK
|
| 76 |
+
if application.vi_state.input_mode in {
|
| 77 |
+
InputMode.INSERT,
|
| 78 |
+
InputMode.INSERT_MULTIPLE,
|
| 79 |
+
}:
|
| 80 |
+
return CursorShape.BEAM
|
| 81 |
+
if application.vi_state.input_mode in {
|
| 82 |
+
InputMode.REPLACE,
|
| 83 |
+
InputMode.REPLACE_SINGLE,
|
| 84 |
+
}:
|
| 85 |
+
return CursorShape.UNDERLINE
|
| 86 |
+
elif application.editing_mode == EditingMode.EMACS:
|
| 87 |
+
# like vi's INSERT
|
| 88 |
+
return CursorShape.BEAM
|
| 89 |
+
|
| 90 |
+
# Default
|
| 91 |
+
return CursorShape.BLOCK
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class DynamicCursorShapeConfig(CursorShapeConfig):
|
| 95 |
+
def __init__(
|
| 96 |
+
self, get_cursor_shape_config: Callable[[], AnyCursorShapeConfig]
|
| 97 |
+
) -> None:
|
| 98 |
+
self.get_cursor_shape_config = get_cursor_shape_config
|
| 99 |
+
|
| 100 |
+
def get_cursor_shape(self, application: Application[Any]) -> CursorShape:
|
| 101 |
+
return to_cursor_shape_config(self.get_cursor_shape_config()).get_cursor_shape(
|
| 102 |
+
application
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def to_cursor_shape_config(value: AnyCursorShapeConfig) -> CursorShapeConfig:
|
| 107 |
+
"""
|
| 108 |
+
Take a `CursorShape` instance or `CursorShapeConfig` and turn it into a
|
| 109 |
+
`CursorShapeConfig`.
|
| 110 |
+
"""
|
| 111 |
+
if value is None:
|
| 112 |
+
return SimpleCursorShapeConfig()
|
| 113 |
+
|
| 114 |
+
if isinstance(value, CursorShape):
|
| 115 |
+
return SimpleCursorShapeConfig(value)
|
| 116 |
+
|
| 117 |
+
return value
|
lib/python3.12/site-packages/prompt_toolkit/data_structures.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import NamedTuple
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"Point",
|
| 7 |
+
"Size",
|
| 8 |
+
]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Point(NamedTuple):
|
| 12 |
+
x: int
|
| 13 |
+
y: int
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Size(NamedTuple):
|
| 17 |
+
rows: int
|
| 18 |
+
columns: int
|
lib/python3.12/site-packages/prompt_toolkit/document.py
ADDED
|
@@ -0,0 +1,1182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
The `Document` that implements all the text operations/querying.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import bisect
|
| 8 |
+
import re
|
| 9 |
+
import string
|
| 10 |
+
import weakref
|
| 11 |
+
from typing import Callable, Dict, Iterable, List, NoReturn, Pattern, cast
|
| 12 |
+
|
| 13 |
+
from .clipboard import ClipboardData
|
| 14 |
+
from .filters import vi_mode
|
| 15 |
+
from .selection import PasteMode, SelectionState, SelectionType
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
"Document",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Regex for finding "words" in documents. (We consider a group of alnum
|
| 23 |
+
# characters a word, but also a group of special characters a word, as long as
|
| 24 |
+
# it doesn't contain a space.)
|
| 25 |
+
# (This is a 'word' in Vi.)
|
| 26 |
+
_FIND_WORD_RE = re.compile(r"([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)")
|
| 27 |
+
_FIND_CURRENT_WORD_RE = re.compile(r"^([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)")
|
| 28 |
+
_FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile(
|
| 29 |
+
r"^(([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)\s*)"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Regex for finding "WORDS" in documents.
|
| 33 |
+
# (This is a 'WORD in Vi.)
|
| 34 |
+
_FIND_BIG_WORD_RE = re.compile(r"([^\s]+)")
|
| 35 |
+
_FIND_CURRENT_BIG_WORD_RE = re.compile(r"^([^\s]+)")
|
| 36 |
+
_FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile(r"^([^\s]+\s*)")
|
| 37 |
+
|
| 38 |
+
# Share the Document._cache between all Document instances.
|
| 39 |
+
# (Document instances are considered immutable. That means that if another
|
| 40 |
+
# `Document` is constructed with the same text, it should have the same
|
| 41 |
+
# `_DocumentCache`.)
|
| 42 |
+
_text_to_document_cache: dict[str, _DocumentCache] = cast(
|
| 43 |
+
Dict[str, "_DocumentCache"],
|
| 44 |
+
weakref.WeakValueDictionary(), # Maps document.text to DocumentCache instance.
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class _ImmutableLineList(List[str]):
|
| 49 |
+
"""
|
| 50 |
+
Some protection for our 'lines' list, which is assumed to be immutable in the cache.
|
| 51 |
+
(Useful for detecting obvious bugs.)
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def _error(self, *a: object, **kw: object) -> NoReturn:
|
| 55 |
+
raise NotImplementedError("Attempt to modify an immutable list.")
|
| 56 |
+
|
| 57 |
+
__setitem__ = _error
|
| 58 |
+
append = _error
|
| 59 |
+
clear = _error
|
| 60 |
+
extend = _error
|
| 61 |
+
insert = _error
|
| 62 |
+
pop = _error
|
| 63 |
+
remove = _error
|
| 64 |
+
reverse = _error
|
| 65 |
+
sort = _error
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class _DocumentCache:
|
| 69 |
+
def __init__(self) -> None:
|
| 70 |
+
#: List of lines for the Document text.
|
| 71 |
+
self.lines: _ImmutableLineList | None = None
|
| 72 |
+
|
| 73 |
+
#: List of index positions, pointing to the start of all the lines.
|
| 74 |
+
self.line_indexes: list[int] | None = None
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class Document:
|
| 78 |
+
"""
|
| 79 |
+
This is a immutable class around the text and cursor position, and contains
|
| 80 |
+
methods for querying this data, e.g. to give the text before the cursor.
|
| 81 |
+
|
| 82 |
+
This class is usually instantiated by a :class:`~prompt_toolkit.buffer.Buffer`
|
| 83 |
+
object, and accessed as the `document` property of that class.
|
| 84 |
+
|
| 85 |
+
:param text: string
|
| 86 |
+
:param cursor_position: int
|
| 87 |
+
:param selection: :class:`.SelectionState`
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
__slots__ = ("_text", "_cursor_position", "_selection", "_cache")
|
| 91 |
+
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
text: str = "",
|
| 95 |
+
cursor_position: int | None = None,
|
| 96 |
+
selection: SelectionState | None = None,
|
| 97 |
+
) -> None:
|
| 98 |
+
# Check cursor position. It can also be right after the end. (Where we
|
| 99 |
+
# insert text.)
|
| 100 |
+
assert cursor_position is None or cursor_position <= len(text), AssertionError(
|
| 101 |
+
f"cursor_position={cursor_position!r}, len_text={len(text)!r}"
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# By default, if no cursor position was given, make sure to put the
|
| 105 |
+
# cursor position is at the end of the document. This is what makes
|
| 106 |
+
# sense in most places.
|
| 107 |
+
if cursor_position is None:
|
| 108 |
+
cursor_position = len(text)
|
| 109 |
+
|
| 110 |
+
# Keep these attributes private. A `Document` really has to be
|
| 111 |
+
# considered to be immutable, because otherwise the caching will break
|
| 112 |
+
# things. Because of that, we wrap these into read-only properties.
|
| 113 |
+
self._text = text
|
| 114 |
+
self._cursor_position = cursor_position
|
| 115 |
+
self._selection = selection
|
| 116 |
+
|
| 117 |
+
# Cache for lines/indexes. (Shared with other Document instances that
|
| 118 |
+
# contain the same text.
|
| 119 |
+
try:
|
| 120 |
+
self._cache = _text_to_document_cache[self.text]
|
| 121 |
+
except KeyError:
|
| 122 |
+
self._cache = _DocumentCache()
|
| 123 |
+
_text_to_document_cache[self.text] = self._cache
|
| 124 |
+
|
| 125 |
+
# XX: For some reason, above, we can't use 'WeakValueDictionary.setdefault'.
|
| 126 |
+
# This fails in Pypy3. `self._cache` becomes None, because that's what
|
| 127 |
+
# 'setdefault' returns.
|
| 128 |
+
# self._cache = _text_to_document_cache.setdefault(self.text, _DocumentCache())
|
| 129 |
+
# assert self._cache
|
| 130 |
+
|
| 131 |
+
def __repr__(self) -> str:
|
| 132 |
+
return f"{self.__class__.__name__}({self.text!r}, {self.cursor_position!r})"
|
| 133 |
+
|
| 134 |
+
def __eq__(self, other: object) -> bool:
|
| 135 |
+
if not isinstance(other, Document):
|
| 136 |
+
return False
|
| 137 |
+
|
| 138 |
+
return (
|
| 139 |
+
self.text == other.text
|
| 140 |
+
and self.cursor_position == other.cursor_position
|
| 141 |
+
and self.selection == other.selection
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
@property
|
| 145 |
+
def text(self) -> str:
|
| 146 |
+
"The document text."
|
| 147 |
+
return self._text
|
| 148 |
+
|
| 149 |
+
@property
|
| 150 |
+
def cursor_position(self) -> int:
|
| 151 |
+
"The document cursor position."
|
| 152 |
+
return self._cursor_position
|
| 153 |
+
|
| 154 |
+
@property
|
| 155 |
+
def selection(self) -> SelectionState | None:
|
| 156 |
+
":class:`.SelectionState` object."
|
| 157 |
+
return self._selection
|
| 158 |
+
|
| 159 |
+
@property
|
| 160 |
+
def current_char(self) -> str:
|
| 161 |
+
"""Return character under cursor or an empty string."""
|
| 162 |
+
return self._get_char_relative_to_cursor(0) or ""
|
| 163 |
+
|
| 164 |
+
@property
|
| 165 |
+
def char_before_cursor(self) -> str:
|
| 166 |
+
"""Return character before the cursor or an empty string."""
|
| 167 |
+
return self._get_char_relative_to_cursor(-1) or ""
|
| 168 |
+
|
| 169 |
+
@property
|
| 170 |
+
def text_before_cursor(self) -> str:
|
| 171 |
+
return self.text[: self.cursor_position :]
|
| 172 |
+
|
| 173 |
+
@property
|
| 174 |
+
def text_after_cursor(self) -> str:
|
| 175 |
+
return self.text[self.cursor_position :]
|
| 176 |
+
|
| 177 |
+
@property
|
| 178 |
+
def current_line_before_cursor(self) -> str:
|
| 179 |
+
"""Text from the start of the line until the cursor."""
|
| 180 |
+
_, _, text = self.text_before_cursor.rpartition("\n")
|
| 181 |
+
return text
|
| 182 |
+
|
| 183 |
+
@property
|
| 184 |
+
def current_line_after_cursor(self) -> str:
|
| 185 |
+
"""Text from the cursor until the end of the line."""
|
| 186 |
+
text, _, _ = self.text_after_cursor.partition("\n")
|
| 187 |
+
return text
|
| 188 |
+
|
| 189 |
+
@property
|
| 190 |
+
def lines(self) -> list[str]:
|
| 191 |
+
"""
|
| 192 |
+
Array of all the lines.
|
| 193 |
+
"""
|
| 194 |
+
# Cache, because this one is reused very often.
|
| 195 |
+
if self._cache.lines is None:
|
| 196 |
+
self._cache.lines = _ImmutableLineList(self.text.split("\n"))
|
| 197 |
+
|
| 198 |
+
return self._cache.lines
|
| 199 |
+
|
| 200 |
+
@property
|
| 201 |
+
def _line_start_indexes(self) -> list[int]:
|
| 202 |
+
"""
|
| 203 |
+
Array pointing to the start indexes of all the lines.
|
| 204 |
+
"""
|
| 205 |
+
# Cache, because this is often reused. (If it is used, it's often used
|
| 206 |
+
# many times. And this has to be fast for editing big documents!)
|
| 207 |
+
if self._cache.line_indexes is None:
|
| 208 |
+
# Create list of line lengths.
|
| 209 |
+
line_lengths = map(len, self.lines)
|
| 210 |
+
|
| 211 |
+
# Calculate cumulative sums.
|
| 212 |
+
indexes = [0]
|
| 213 |
+
append = indexes.append
|
| 214 |
+
pos = 0
|
| 215 |
+
|
| 216 |
+
for line_length in line_lengths:
|
| 217 |
+
pos += line_length + 1
|
| 218 |
+
append(pos)
|
| 219 |
+
|
| 220 |
+
# Remove the last item. (This is not a new line.)
|
| 221 |
+
if len(indexes) > 1:
|
| 222 |
+
indexes.pop()
|
| 223 |
+
|
| 224 |
+
self._cache.line_indexes = indexes
|
| 225 |
+
|
| 226 |
+
return self._cache.line_indexes
|
| 227 |
+
|
| 228 |
+
@property
|
| 229 |
+
def lines_from_current(self) -> list[str]:
|
| 230 |
+
"""
|
| 231 |
+
Array of the lines starting from the current line, until the last line.
|
| 232 |
+
"""
|
| 233 |
+
return self.lines[self.cursor_position_row :]
|
| 234 |
+
|
| 235 |
+
@property
|
| 236 |
+
def line_count(self) -> int:
|
| 237 |
+
r"""Return the number of lines in this document. If the document ends
|
| 238 |
+
with a trailing \n, that counts as the beginning of a new line."""
|
| 239 |
+
return len(self.lines)
|
| 240 |
+
|
| 241 |
+
@property
|
| 242 |
+
def current_line(self) -> str:
|
| 243 |
+
"""Return the text on the line where the cursor is. (when the input
|
| 244 |
+
consists of just one line, it equals `text`."""
|
| 245 |
+
return self.current_line_before_cursor + self.current_line_after_cursor
|
| 246 |
+
|
| 247 |
+
@property
|
| 248 |
+
def leading_whitespace_in_current_line(self) -> str:
|
| 249 |
+
"""The leading whitespace in the left margin of the current line."""
|
| 250 |
+
current_line = self.current_line
|
| 251 |
+
length = len(current_line) - len(current_line.lstrip())
|
| 252 |
+
return current_line[:length]
|
| 253 |
+
|
| 254 |
+
def _get_char_relative_to_cursor(self, offset: int = 0) -> str:
|
| 255 |
+
"""
|
| 256 |
+
Return character relative to cursor position, or empty string
|
| 257 |
+
"""
|
| 258 |
+
try:
|
| 259 |
+
return self.text[self.cursor_position + offset]
|
| 260 |
+
except IndexError:
|
| 261 |
+
return ""
|
| 262 |
+
|
| 263 |
+
@property
|
| 264 |
+
def on_first_line(self) -> bool:
|
| 265 |
+
"""
|
| 266 |
+
True when we are at the first line.
|
| 267 |
+
"""
|
| 268 |
+
return self.cursor_position_row == 0
|
| 269 |
+
|
| 270 |
+
@property
|
| 271 |
+
def on_last_line(self) -> bool:
|
| 272 |
+
"""
|
| 273 |
+
True when we are at the last line.
|
| 274 |
+
"""
|
| 275 |
+
return self.cursor_position_row == self.line_count - 1
|
| 276 |
+
|
| 277 |
+
@property
|
| 278 |
+
def cursor_position_row(self) -> int:
|
| 279 |
+
"""
|
| 280 |
+
Current row. (0-based.)
|
| 281 |
+
"""
|
| 282 |
+
row, _ = self._find_line_start_index(self.cursor_position)
|
| 283 |
+
return row
|
| 284 |
+
|
| 285 |
+
@property
|
| 286 |
+
def cursor_position_col(self) -> int:
|
| 287 |
+
"""
|
| 288 |
+
Current column. (0-based.)
|
| 289 |
+
"""
|
| 290 |
+
# (Don't use self.text_before_cursor to calculate this. Creating
|
| 291 |
+
# substrings and doing rsplit is too expensive for getting the cursor
|
| 292 |
+
# position.)
|
| 293 |
+
_, line_start_index = self._find_line_start_index(self.cursor_position)
|
| 294 |
+
return self.cursor_position - line_start_index
|
| 295 |
+
|
| 296 |
+
def _find_line_start_index(self, index: int) -> tuple[int, int]:
|
| 297 |
+
"""
|
| 298 |
+
For the index of a character at a certain line, calculate the index of
|
| 299 |
+
the first character on that line.
|
| 300 |
+
|
| 301 |
+
Return (row, index) tuple.
|
| 302 |
+
"""
|
| 303 |
+
indexes = self._line_start_indexes
|
| 304 |
+
|
| 305 |
+
pos = bisect.bisect_right(indexes, index) - 1
|
| 306 |
+
return pos, indexes[pos]
|
| 307 |
+
|
| 308 |
+
def translate_index_to_position(self, index: int) -> tuple[int, int]:
|
| 309 |
+
"""
|
| 310 |
+
Given an index for the text, return the corresponding (row, col) tuple.
|
| 311 |
+
(0-based. Returns (0, 0) for index=0.)
|
| 312 |
+
"""
|
| 313 |
+
# Find start of this line.
|
| 314 |
+
row, row_index = self._find_line_start_index(index)
|
| 315 |
+
col = index - row_index
|
| 316 |
+
|
| 317 |
+
return row, col
|
| 318 |
+
|
| 319 |
+
def translate_row_col_to_index(self, row: int, col: int) -> int:
|
| 320 |
+
"""
|
| 321 |
+
Given a (row, col) tuple, return the corresponding index.
|
| 322 |
+
(Row and col params are 0-based.)
|
| 323 |
+
|
| 324 |
+
Negative row/col values are turned into zero.
|
| 325 |
+
"""
|
| 326 |
+
try:
|
| 327 |
+
result = self._line_start_indexes[row]
|
| 328 |
+
line = self.lines[row]
|
| 329 |
+
except IndexError:
|
| 330 |
+
if row < 0:
|
| 331 |
+
result = self._line_start_indexes[0]
|
| 332 |
+
line = self.lines[0]
|
| 333 |
+
else:
|
| 334 |
+
result = self._line_start_indexes[-1]
|
| 335 |
+
line = self.lines[-1]
|
| 336 |
+
|
| 337 |
+
result += max(0, min(col, len(line)))
|
| 338 |
+
|
| 339 |
+
# Keep in range. (len(self.text) is included, because the cursor can be
|
| 340 |
+
# right after the end of the text as well.)
|
| 341 |
+
result = max(0, min(result, len(self.text)))
|
| 342 |
+
return result
|
| 343 |
+
|
| 344 |
+
@property
|
| 345 |
+
def is_cursor_at_the_end(self) -> bool:
|
| 346 |
+
"""True when the cursor is at the end of the text."""
|
| 347 |
+
return self.cursor_position == len(self.text)
|
| 348 |
+
|
| 349 |
+
@property
|
| 350 |
+
def is_cursor_at_the_end_of_line(self) -> bool:
|
| 351 |
+
"""True when the cursor is at the end of this line."""
|
| 352 |
+
return self.current_char in ("\n", "")
|
| 353 |
+
|
| 354 |
+
def has_match_at_current_position(self, sub: str) -> bool:
|
| 355 |
+
"""
|
| 356 |
+
`True` when this substring is found at the cursor position.
|
| 357 |
+
"""
|
| 358 |
+
return self.text.find(sub, self.cursor_position) == self.cursor_position
|
| 359 |
+
|
| 360 |
+
def find(
|
| 361 |
+
self,
|
| 362 |
+
sub: str,
|
| 363 |
+
in_current_line: bool = False,
|
| 364 |
+
include_current_position: bool = False,
|
| 365 |
+
ignore_case: bool = False,
|
| 366 |
+
count: int = 1,
|
| 367 |
+
) -> int | None:
|
| 368 |
+
"""
|
| 369 |
+
Find `text` after the cursor, return position relative to the cursor
|
| 370 |
+
position. Return `None` if nothing was found.
|
| 371 |
+
|
| 372 |
+
:param count: Find the n-th occurrence.
|
| 373 |
+
"""
|
| 374 |
+
assert isinstance(ignore_case, bool)
|
| 375 |
+
|
| 376 |
+
if in_current_line:
|
| 377 |
+
text = self.current_line_after_cursor
|
| 378 |
+
else:
|
| 379 |
+
text = self.text_after_cursor
|
| 380 |
+
|
| 381 |
+
if not include_current_position:
|
| 382 |
+
if len(text) == 0:
|
| 383 |
+
return None # (Otherwise, we always get a match for the empty string.)
|
| 384 |
+
else:
|
| 385 |
+
text = text[1:]
|
| 386 |
+
|
| 387 |
+
flags = re.IGNORECASE if ignore_case else 0
|
| 388 |
+
iterator = re.finditer(re.escape(sub), text, flags)
|
| 389 |
+
|
| 390 |
+
try:
|
| 391 |
+
for i, match in enumerate(iterator):
|
| 392 |
+
if i + 1 == count:
|
| 393 |
+
if include_current_position:
|
| 394 |
+
return match.start(0)
|
| 395 |
+
else:
|
| 396 |
+
return match.start(0) + 1
|
| 397 |
+
except StopIteration:
|
| 398 |
+
pass
|
| 399 |
+
return None
|
| 400 |
+
|
| 401 |
+
def find_all(self, sub: str, ignore_case: bool = False) -> list[int]:
|
| 402 |
+
"""
|
| 403 |
+
Find all occurrences of the substring. Return a list of absolute
|
| 404 |
+
positions in the document.
|
| 405 |
+
"""
|
| 406 |
+
flags = re.IGNORECASE if ignore_case else 0
|
| 407 |
+
return [a.start() for a in re.finditer(re.escape(sub), self.text, flags)]
|
| 408 |
+
|
| 409 |
+
def find_backwards(
|
| 410 |
+
self,
|
| 411 |
+
sub: str,
|
| 412 |
+
in_current_line: bool = False,
|
| 413 |
+
ignore_case: bool = False,
|
| 414 |
+
count: int = 1,
|
| 415 |
+
) -> int | None:
|
| 416 |
+
"""
|
| 417 |
+
Find `text` before the cursor, return position relative to the cursor
|
| 418 |
+
position. Return `None` if nothing was found.
|
| 419 |
+
|
| 420 |
+
:param count: Find the n-th occurrence.
|
| 421 |
+
"""
|
| 422 |
+
if in_current_line:
|
| 423 |
+
before_cursor = self.current_line_before_cursor[::-1]
|
| 424 |
+
else:
|
| 425 |
+
before_cursor = self.text_before_cursor[::-1]
|
| 426 |
+
|
| 427 |
+
flags = re.IGNORECASE if ignore_case else 0
|
| 428 |
+
iterator = re.finditer(re.escape(sub[::-1]), before_cursor, flags)
|
| 429 |
+
|
| 430 |
+
try:
|
| 431 |
+
for i, match in enumerate(iterator):
|
| 432 |
+
if i + 1 == count:
|
| 433 |
+
return -match.start(0) - len(sub)
|
| 434 |
+
except StopIteration:
|
| 435 |
+
pass
|
| 436 |
+
return None
|
| 437 |
+
|
| 438 |
+
def get_word_before_cursor(
|
| 439 |
+
self, WORD: bool = False, pattern: Pattern[str] | None = None
|
| 440 |
+
) -> str:
|
| 441 |
+
"""
|
| 442 |
+
Give the word before the cursor.
|
| 443 |
+
If we have whitespace before the cursor this returns an empty string.
|
| 444 |
+
|
| 445 |
+
:param pattern: (None or compiled regex). When given, use this regex
|
| 446 |
+
pattern.
|
| 447 |
+
"""
|
| 448 |
+
if self._is_word_before_cursor_complete(WORD=WORD, pattern=pattern):
|
| 449 |
+
# Space before the cursor or no text before cursor.
|
| 450 |
+
return ""
|
| 451 |
+
|
| 452 |
+
text_before_cursor = self.text_before_cursor
|
| 453 |
+
start = self.find_start_of_previous_word(WORD=WORD, pattern=pattern) or 0
|
| 454 |
+
|
| 455 |
+
return text_before_cursor[len(text_before_cursor) + start :]
|
| 456 |
+
|
| 457 |
+
def _is_word_before_cursor_complete(
|
| 458 |
+
self, WORD: bool = False, pattern: Pattern[str] | None = None
|
| 459 |
+
) -> bool:
|
| 460 |
+
if pattern:
|
| 461 |
+
return self.find_start_of_previous_word(WORD=WORD, pattern=pattern) is None
|
| 462 |
+
else:
|
| 463 |
+
return (
|
| 464 |
+
self.text_before_cursor == "" or self.text_before_cursor[-1:].isspace()
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
def find_start_of_previous_word(
|
| 468 |
+
self, count: int = 1, WORD: bool = False, pattern: Pattern[str] | None = None
|
| 469 |
+
) -> int | None:
|
| 470 |
+
"""
|
| 471 |
+
Return an index relative to the cursor position pointing to the start
|
| 472 |
+
of the previous word. Return `None` if nothing was found.
|
| 473 |
+
|
| 474 |
+
:param pattern: (None or compiled regex). When given, use this regex
|
| 475 |
+
pattern.
|
| 476 |
+
"""
|
| 477 |
+
assert not (WORD and pattern)
|
| 478 |
+
|
| 479 |
+
# Reverse the text before the cursor, in order to do an efficient
|
| 480 |
+
# backwards search.
|
| 481 |
+
text_before_cursor = self.text_before_cursor[::-1]
|
| 482 |
+
|
| 483 |
+
if pattern:
|
| 484 |
+
regex = pattern
|
| 485 |
+
elif WORD:
|
| 486 |
+
regex = _FIND_BIG_WORD_RE
|
| 487 |
+
else:
|
| 488 |
+
regex = _FIND_WORD_RE
|
| 489 |
+
|
| 490 |
+
iterator = regex.finditer(text_before_cursor)
|
| 491 |
+
|
| 492 |
+
try:
|
| 493 |
+
for i, match in enumerate(iterator):
|
| 494 |
+
if i + 1 == count:
|
| 495 |
+
return -match.end(0)
|
| 496 |
+
except StopIteration:
|
| 497 |
+
pass
|
| 498 |
+
return None
|
| 499 |
+
|
| 500 |
+
def find_boundaries_of_current_word(
|
| 501 |
+
self,
|
| 502 |
+
WORD: bool = False,
|
| 503 |
+
include_leading_whitespace: bool = False,
|
| 504 |
+
include_trailing_whitespace: bool = False,
|
| 505 |
+
) -> tuple[int, int]:
|
| 506 |
+
"""
|
| 507 |
+
Return the relative boundaries (startpos, endpos) of the current word under the
|
| 508 |
+
cursor. (This is at the current line, because line boundaries obviously
|
| 509 |
+
don't belong to any word.)
|
| 510 |
+
If not on a word, this returns (0,0)
|
| 511 |
+
"""
|
| 512 |
+
text_before_cursor = self.current_line_before_cursor[::-1]
|
| 513 |
+
text_after_cursor = self.current_line_after_cursor
|
| 514 |
+
|
| 515 |
+
def get_regex(include_whitespace: bool) -> Pattern[str]:
|
| 516 |
+
return {
|
| 517 |
+
(False, False): _FIND_CURRENT_WORD_RE,
|
| 518 |
+
(False, True): _FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE,
|
| 519 |
+
(True, False): _FIND_CURRENT_BIG_WORD_RE,
|
| 520 |
+
(True, True): _FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE,
|
| 521 |
+
}[(WORD, include_whitespace)]
|
| 522 |
+
|
| 523 |
+
match_before = get_regex(include_leading_whitespace).search(text_before_cursor)
|
| 524 |
+
match_after = get_regex(include_trailing_whitespace).search(text_after_cursor)
|
| 525 |
+
|
| 526 |
+
# When there is a match before and after, and we're not looking for
|
| 527 |
+
# WORDs, make sure that both the part before and after the cursor are
|
| 528 |
+
# either in the [a-zA-Z_] alphabet or not. Otherwise, drop the part
|
| 529 |
+
# before the cursor.
|
| 530 |
+
if not WORD and match_before and match_after:
|
| 531 |
+
c1 = self.text[self.cursor_position - 1]
|
| 532 |
+
c2 = self.text[self.cursor_position]
|
| 533 |
+
alphabet = string.ascii_letters + "0123456789_"
|
| 534 |
+
|
| 535 |
+
if (c1 in alphabet) != (c2 in alphabet):
|
| 536 |
+
match_before = None
|
| 537 |
+
|
| 538 |
+
return (
|
| 539 |
+
-match_before.end(1) if match_before else 0,
|
| 540 |
+
match_after.end(1) if match_after else 0,
|
| 541 |
+
)
|
| 542 |
+
|
| 543 |
+
def get_word_under_cursor(self, WORD: bool = False) -> str:
|
| 544 |
+
"""
|
| 545 |
+
Return the word, currently below the cursor.
|
| 546 |
+
This returns an empty string when the cursor is on a whitespace region.
|
| 547 |
+
"""
|
| 548 |
+
start, end = self.find_boundaries_of_current_word(WORD=WORD)
|
| 549 |
+
return self.text[self.cursor_position + start : self.cursor_position + end]
|
| 550 |
+
|
| 551 |
+
def find_next_word_beginning(
|
| 552 |
+
self, count: int = 1, WORD: bool = False
|
| 553 |
+
) -> int | None:
|
| 554 |
+
"""
|
| 555 |
+
Return an index relative to the cursor position pointing to the start
|
| 556 |
+
of the next word. Return `None` if nothing was found.
|
| 557 |
+
"""
|
| 558 |
+
if count < 0:
|
| 559 |
+
return self.find_previous_word_beginning(count=-count, WORD=WORD)
|
| 560 |
+
|
| 561 |
+
regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE
|
| 562 |
+
iterator = regex.finditer(self.text_after_cursor)
|
| 563 |
+
|
| 564 |
+
try:
|
| 565 |
+
for i, match in enumerate(iterator):
|
| 566 |
+
# Take first match, unless it's the word on which we're right now.
|
| 567 |
+
if i == 0 and match.start(1) == 0:
|
| 568 |
+
count += 1
|
| 569 |
+
|
| 570 |
+
if i + 1 == count:
|
| 571 |
+
return match.start(1)
|
| 572 |
+
except StopIteration:
|
| 573 |
+
pass
|
| 574 |
+
return None
|
| 575 |
+
|
| 576 |
+
def find_next_word_ending(
|
| 577 |
+
self, include_current_position: bool = False, count: int = 1, WORD: bool = False
|
| 578 |
+
) -> int | None:
|
| 579 |
+
"""
|
| 580 |
+
Return an index relative to the cursor position pointing to the end
|
| 581 |
+
of the next word. Return `None` if nothing was found.
|
| 582 |
+
"""
|
| 583 |
+
if count < 0:
|
| 584 |
+
return self.find_previous_word_ending(count=-count, WORD=WORD)
|
| 585 |
+
|
| 586 |
+
if include_current_position:
|
| 587 |
+
text = self.text_after_cursor
|
| 588 |
+
else:
|
| 589 |
+
text = self.text_after_cursor[1:]
|
| 590 |
+
|
| 591 |
+
regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE
|
| 592 |
+
iterable = regex.finditer(text)
|
| 593 |
+
|
| 594 |
+
try:
|
| 595 |
+
for i, match in enumerate(iterable):
|
| 596 |
+
if i + 1 == count:
|
| 597 |
+
value = match.end(1)
|
| 598 |
+
|
| 599 |
+
if include_current_position:
|
| 600 |
+
return value
|
| 601 |
+
else:
|
| 602 |
+
return value + 1
|
| 603 |
+
|
| 604 |
+
except StopIteration:
|
| 605 |
+
pass
|
| 606 |
+
return None
|
| 607 |
+
|
| 608 |
+
def find_previous_word_beginning(
|
| 609 |
+
self, count: int = 1, WORD: bool = False
|
| 610 |
+
) -> int | None:
|
| 611 |
+
"""
|
| 612 |
+
Return an index relative to the cursor position pointing to the start
|
| 613 |
+
of the previous word. Return `None` if nothing was found.
|
| 614 |
+
"""
|
| 615 |
+
if count < 0:
|
| 616 |
+
return self.find_next_word_beginning(count=-count, WORD=WORD)
|
| 617 |
+
|
| 618 |
+
regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE
|
| 619 |
+
iterator = regex.finditer(self.text_before_cursor[::-1])
|
| 620 |
+
|
| 621 |
+
try:
|
| 622 |
+
for i, match in enumerate(iterator):
|
| 623 |
+
if i + 1 == count:
|
| 624 |
+
return -match.end(1)
|
| 625 |
+
except StopIteration:
|
| 626 |
+
pass
|
| 627 |
+
return None
|
| 628 |
+
|
| 629 |
+
def find_previous_word_ending(
|
| 630 |
+
self, count: int = 1, WORD: bool = False
|
| 631 |
+
) -> int | None:
|
| 632 |
+
"""
|
| 633 |
+
Return an index relative to the cursor position pointing to the end
|
| 634 |
+
of the previous word. Return `None` if nothing was found.
|
| 635 |
+
"""
|
| 636 |
+
if count < 0:
|
| 637 |
+
return self.find_next_word_ending(count=-count, WORD=WORD)
|
| 638 |
+
|
| 639 |
+
text_before_cursor = self.text_after_cursor[:1] + self.text_before_cursor[::-1]
|
| 640 |
+
|
| 641 |
+
regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE
|
| 642 |
+
iterator = regex.finditer(text_before_cursor)
|
| 643 |
+
|
| 644 |
+
try:
|
| 645 |
+
for i, match in enumerate(iterator):
|
| 646 |
+
# Take first match, unless it's the word on which we're right now.
|
| 647 |
+
if i == 0 and match.start(1) == 0:
|
| 648 |
+
count += 1
|
| 649 |
+
|
| 650 |
+
if i + 1 == count:
|
| 651 |
+
return -match.start(1) + 1
|
| 652 |
+
except StopIteration:
|
| 653 |
+
pass
|
| 654 |
+
return None
|
| 655 |
+
|
| 656 |
+
def find_next_matching_line(
|
| 657 |
+
self, match_func: Callable[[str], bool], count: int = 1
|
| 658 |
+
) -> int | None:
|
| 659 |
+
"""
|
| 660 |
+
Look downwards for empty lines.
|
| 661 |
+
Return the line index, relative to the current line.
|
| 662 |
+
"""
|
| 663 |
+
result = None
|
| 664 |
+
|
| 665 |
+
for index, line in enumerate(self.lines[self.cursor_position_row + 1 :]):
|
| 666 |
+
if match_func(line):
|
| 667 |
+
result = 1 + index
|
| 668 |
+
count -= 1
|
| 669 |
+
|
| 670 |
+
if count == 0:
|
| 671 |
+
break
|
| 672 |
+
|
| 673 |
+
return result
|
| 674 |
+
|
| 675 |
+
def find_previous_matching_line(
|
| 676 |
+
self, match_func: Callable[[str], bool], count: int = 1
|
| 677 |
+
) -> int | None:
|
| 678 |
+
"""
|
| 679 |
+
Look upwards for empty lines.
|
| 680 |
+
Return the line index, relative to the current line.
|
| 681 |
+
"""
|
| 682 |
+
result = None
|
| 683 |
+
|
| 684 |
+
for index, line in enumerate(self.lines[: self.cursor_position_row][::-1]):
|
| 685 |
+
if match_func(line):
|
| 686 |
+
result = -1 - index
|
| 687 |
+
count -= 1
|
| 688 |
+
|
| 689 |
+
if count == 0:
|
| 690 |
+
break
|
| 691 |
+
|
| 692 |
+
return result
|
| 693 |
+
|
| 694 |
+
def get_cursor_left_position(self, count: int = 1) -> int:
|
| 695 |
+
"""
|
| 696 |
+
Relative position for cursor left.
|
| 697 |
+
"""
|
| 698 |
+
if count < 0:
|
| 699 |
+
return self.get_cursor_right_position(-count)
|
| 700 |
+
|
| 701 |
+
return -min(self.cursor_position_col, count)
|
| 702 |
+
|
| 703 |
+
def get_cursor_right_position(self, count: int = 1) -> int:
|
| 704 |
+
"""
|
| 705 |
+
Relative position for cursor_right.
|
| 706 |
+
"""
|
| 707 |
+
if count < 0:
|
| 708 |
+
return self.get_cursor_left_position(-count)
|
| 709 |
+
|
| 710 |
+
return min(count, len(self.current_line_after_cursor))
|
| 711 |
+
|
| 712 |
+
def get_cursor_up_position(
|
| 713 |
+
self, count: int = 1, preferred_column: int | None = None
|
| 714 |
+
) -> int:
|
| 715 |
+
"""
|
| 716 |
+
Return the relative cursor position (character index) where we would be if the
|
| 717 |
+
user pressed the arrow-up button.
|
| 718 |
+
|
| 719 |
+
:param preferred_column: When given, go to this column instead of
|
| 720 |
+
staying at the current column.
|
| 721 |
+
"""
|
| 722 |
+
assert count >= 1
|
| 723 |
+
column = (
|
| 724 |
+
self.cursor_position_col if preferred_column is None else preferred_column
|
| 725 |
+
)
|
| 726 |
+
|
| 727 |
+
return (
|
| 728 |
+
self.translate_row_col_to_index(
|
| 729 |
+
max(0, self.cursor_position_row - count), column
|
| 730 |
+
)
|
| 731 |
+
- self.cursor_position
|
| 732 |
+
)
|
| 733 |
+
|
| 734 |
+
def get_cursor_down_position(
|
| 735 |
+
self, count: int = 1, preferred_column: int | None = None
|
| 736 |
+
) -> int:
|
| 737 |
+
"""
|
| 738 |
+
Return the relative cursor position (character index) where we would be if the
|
| 739 |
+
user pressed the arrow-down button.
|
| 740 |
+
|
| 741 |
+
:param preferred_column: When given, go to this column instead of
|
| 742 |
+
staying at the current column.
|
| 743 |
+
"""
|
| 744 |
+
assert count >= 1
|
| 745 |
+
column = (
|
| 746 |
+
self.cursor_position_col if preferred_column is None else preferred_column
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
return (
|
| 750 |
+
self.translate_row_col_to_index(self.cursor_position_row + count, column)
|
| 751 |
+
- self.cursor_position
|
| 752 |
+
)
|
| 753 |
+
|
| 754 |
+
def find_enclosing_bracket_right(
|
| 755 |
+
self, left_ch: str, right_ch: str, end_pos: int | None = None
|
| 756 |
+
) -> int | None:
|
| 757 |
+
"""
|
| 758 |
+
Find the right bracket enclosing current position. Return the relative
|
| 759 |
+
position to the cursor position.
|
| 760 |
+
|
| 761 |
+
When `end_pos` is given, don't look past the position.
|
| 762 |
+
"""
|
| 763 |
+
if self.current_char == right_ch:
|
| 764 |
+
return 0
|
| 765 |
+
|
| 766 |
+
if end_pos is None:
|
| 767 |
+
end_pos = len(self.text)
|
| 768 |
+
else:
|
| 769 |
+
end_pos = min(len(self.text), end_pos)
|
| 770 |
+
|
| 771 |
+
stack = 1
|
| 772 |
+
|
| 773 |
+
# Look forward.
|
| 774 |
+
for i in range(self.cursor_position + 1, end_pos):
|
| 775 |
+
c = self.text[i]
|
| 776 |
+
|
| 777 |
+
if c == left_ch:
|
| 778 |
+
stack += 1
|
| 779 |
+
elif c == right_ch:
|
| 780 |
+
stack -= 1
|
| 781 |
+
|
| 782 |
+
if stack == 0:
|
| 783 |
+
return i - self.cursor_position
|
| 784 |
+
|
| 785 |
+
return None
|
| 786 |
+
|
| 787 |
+
def find_enclosing_bracket_left(
|
| 788 |
+
self, left_ch: str, right_ch: str, start_pos: int | None = None
|
| 789 |
+
) -> int | None:
|
| 790 |
+
"""
|
| 791 |
+
Find the left bracket enclosing current position. Return the relative
|
| 792 |
+
position to the cursor position.
|
| 793 |
+
|
| 794 |
+
When `start_pos` is given, don't look past the position.
|
| 795 |
+
"""
|
| 796 |
+
if self.current_char == left_ch:
|
| 797 |
+
return 0
|
| 798 |
+
|
| 799 |
+
if start_pos is None:
|
| 800 |
+
start_pos = 0
|
| 801 |
+
else:
|
| 802 |
+
start_pos = max(0, start_pos)
|
| 803 |
+
|
| 804 |
+
stack = 1
|
| 805 |
+
|
| 806 |
+
# Look backward.
|
| 807 |
+
for i in range(self.cursor_position - 1, start_pos - 1, -1):
|
| 808 |
+
c = self.text[i]
|
| 809 |
+
|
| 810 |
+
if c == right_ch:
|
| 811 |
+
stack += 1
|
| 812 |
+
elif c == left_ch:
|
| 813 |
+
stack -= 1
|
| 814 |
+
|
| 815 |
+
if stack == 0:
|
| 816 |
+
return i - self.cursor_position
|
| 817 |
+
|
| 818 |
+
return None
|
| 819 |
+
|
| 820 |
+
def find_matching_bracket_position(
|
| 821 |
+
self, start_pos: int | None = None, end_pos: int | None = None
|
| 822 |
+
) -> int:
|
| 823 |
+
"""
|
| 824 |
+
Return relative cursor position of matching [, (, { or < bracket.
|
| 825 |
+
|
| 826 |
+
When `start_pos` or `end_pos` are given. Don't look past the positions.
|
| 827 |
+
"""
|
| 828 |
+
|
| 829 |
+
# Look for a match.
|
| 830 |
+
for pair in "()", "[]", "{}", "<>":
|
| 831 |
+
A = pair[0]
|
| 832 |
+
B = pair[1]
|
| 833 |
+
if self.current_char == A:
|
| 834 |
+
return self.find_enclosing_bracket_right(A, B, end_pos=end_pos) or 0
|
| 835 |
+
elif self.current_char == B:
|
| 836 |
+
return self.find_enclosing_bracket_left(A, B, start_pos=start_pos) or 0
|
| 837 |
+
|
| 838 |
+
return 0
|
| 839 |
+
|
| 840 |
+
def get_start_of_document_position(self) -> int:
|
| 841 |
+
"""Relative position for the start of the document."""
|
| 842 |
+
return -self.cursor_position
|
| 843 |
+
|
| 844 |
+
def get_end_of_document_position(self) -> int:
|
| 845 |
+
"""Relative position for the end of the document."""
|
| 846 |
+
return len(self.text) - self.cursor_position
|
| 847 |
+
|
| 848 |
+
def get_start_of_line_position(self, after_whitespace: bool = False) -> int:
|
| 849 |
+
"""Relative position for the start of this line."""
|
| 850 |
+
if after_whitespace:
|
| 851 |
+
current_line = self.current_line
|
| 852 |
+
return (
|
| 853 |
+
len(current_line)
|
| 854 |
+
- len(current_line.lstrip())
|
| 855 |
+
- self.cursor_position_col
|
| 856 |
+
)
|
| 857 |
+
else:
|
| 858 |
+
return -len(self.current_line_before_cursor)
|
| 859 |
+
|
| 860 |
+
def get_end_of_line_position(self) -> int:
|
| 861 |
+
"""Relative position for the end of this line."""
|
| 862 |
+
return len(self.current_line_after_cursor)
|
| 863 |
+
|
| 864 |
+
def last_non_blank_of_current_line_position(self) -> int:
|
| 865 |
+
"""
|
| 866 |
+
Relative position for the last non blank character of this line.
|
| 867 |
+
"""
|
| 868 |
+
return len(self.current_line.rstrip()) - self.cursor_position_col - 1
|
| 869 |
+
|
| 870 |
+
def get_column_cursor_position(self, column: int) -> int:
|
| 871 |
+
"""
|
| 872 |
+
Return the relative cursor position for this column at the current
|
| 873 |
+
line. (It will stay between the boundaries of the line in case of a
|
| 874 |
+
larger number.)
|
| 875 |
+
"""
|
| 876 |
+
line_length = len(self.current_line)
|
| 877 |
+
current_column = self.cursor_position_col
|
| 878 |
+
column = max(0, min(line_length, column))
|
| 879 |
+
|
| 880 |
+
return column - current_column
|
| 881 |
+
|
| 882 |
+
def selection_range(
|
| 883 |
+
self,
|
| 884 |
+
) -> tuple[
|
| 885 |
+
int, int
|
| 886 |
+
]: # XXX: shouldn't this return `None` if there is no selection???
|
| 887 |
+
"""
|
| 888 |
+
Return (from, to) tuple of the selection.
|
| 889 |
+
start and end position are included.
|
| 890 |
+
|
| 891 |
+
This doesn't take the selection type into account. Use
|
| 892 |
+
`selection_ranges` instead.
|
| 893 |
+
"""
|
| 894 |
+
if self.selection:
|
| 895 |
+
from_, to = sorted(
|
| 896 |
+
[self.cursor_position, self.selection.original_cursor_position]
|
| 897 |
+
)
|
| 898 |
+
else:
|
| 899 |
+
from_, to = self.cursor_position, self.cursor_position
|
| 900 |
+
|
| 901 |
+
return from_, to
|
| 902 |
+
|
| 903 |
+
def selection_ranges(self) -> Iterable[tuple[int, int]]:
|
| 904 |
+
"""
|
| 905 |
+
Return a list of `(from, to)` tuples for the selection or none if
|
| 906 |
+
nothing was selected. The upper boundary is not included.
|
| 907 |
+
|
| 908 |
+
This will yield several (from, to) tuples in case of a BLOCK selection.
|
| 909 |
+
This will return zero ranges, like (8,8) for empty lines in a block
|
| 910 |
+
selection.
|
| 911 |
+
"""
|
| 912 |
+
if self.selection:
|
| 913 |
+
from_, to = sorted(
|
| 914 |
+
[self.cursor_position, self.selection.original_cursor_position]
|
| 915 |
+
)
|
| 916 |
+
|
| 917 |
+
if self.selection.type == SelectionType.BLOCK:
|
| 918 |
+
from_line, from_column = self.translate_index_to_position(from_)
|
| 919 |
+
to_line, to_column = self.translate_index_to_position(to)
|
| 920 |
+
from_column, to_column = sorted([from_column, to_column])
|
| 921 |
+
lines = self.lines
|
| 922 |
+
|
| 923 |
+
if vi_mode():
|
| 924 |
+
to_column += 1
|
| 925 |
+
|
| 926 |
+
for l in range(from_line, to_line + 1):
|
| 927 |
+
line_length = len(lines[l])
|
| 928 |
+
|
| 929 |
+
if from_column <= line_length:
|
| 930 |
+
yield (
|
| 931 |
+
self.translate_row_col_to_index(l, from_column),
|
| 932 |
+
self.translate_row_col_to_index(
|
| 933 |
+
l, min(line_length, to_column)
|
| 934 |
+
),
|
| 935 |
+
)
|
| 936 |
+
else:
|
| 937 |
+
# In case of a LINES selection, go to the start/end of the lines.
|
| 938 |
+
if self.selection.type == SelectionType.LINES:
|
| 939 |
+
from_ = max(0, self.text.rfind("\n", 0, from_) + 1)
|
| 940 |
+
|
| 941 |
+
if self.text.find("\n", to) >= 0:
|
| 942 |
+
to = self.text.find("\n", to)
|
| 943 |
+
else:
|
| 944 |
+
to = len(self.text) - 1
|
| 945 |
+
|
| 946 |
+
# In Vi mode, the upper boundary is always included. For Emacs,
|
| 947 |
+
# that's not the case.
|
| 948 |
+
if vi_mode():
|
| 949 |
+
to += 1
|
| 950 |
+
|
| 951 |
+
yield from_, to
|
| 952 |
+
|
| 953 |
+
def selection_range_at_line(self, row: int) -> tuple[int, int] | None:
|
| 954 |
+
"""
|
| 955 |
+
If the selection spans a portion of the given line, return a (from, to) tuple.
|
| 956 |
+
|
| 957 |
+
The returned upper boundary is not included in the selection, so
|
| 958 |
+
`(0, 0)` is an empty selection. `(0, 1)`, is a one character selection.
|
| 959 |
+
|
| 960 |
+
Returns None if the selection doesn't cover this line at all.
|
| 961 |
+
"""
|
| 962 |
+
if self.selection:
|
| 963 |
+
line = self.lines[row]
|
| 964 |
+
|
| 965 |
+
row_start = self.translate_row_col_to_index(row, 0)
|
| 966 |
+
row_end = self.translate_row_col_to_index(row, len(line))
|
| 967 |
+
|
| 968 |
+
from_, to = sorted(
|
| 969 |
+
[self.cursor_position, self.selection.original_cursor_position]
|
| 970 |
+
)
|
| 971 |
+
|
| 972 |
+
# Take the intersection of the current line and the selection.
|
| 973 |
+
intersection_start = max(row_start, from_)
|
| 974 |
+
intersection_end = min(row_end, to)
|
| 975 |
+
|
| 976 |
+
if intersection_start <= intersection_end:
|
| 977 |
+
if self.selection.type == SelectionType.LINES:
|
| 978 |
+
intersection_start = row_start
|
| 979 |
+
intersection_end = row_end
|
| 980 |
+
|
| 981 |
+
elif self.selection.type == SelectionType.BLOCK:
|
| 982 |
+
_, col1 = self.translate_index_to_position(from_)
|
| 983 |
+
_, col2 = self.translate_index_to_position(to)
|
| 984 |
+
col1, col2 = sorted([col1, col2])
|
| 985 |
+
|
| 986 |
+
if col1 > len(line):
|
| 987 |
+
return None # Block selection doesn't cross this line.
|
| 988 |
+
|
| 989 |
+
intersection_start = self.translate_row_col_to_index(row, col1)
|
| 990 |
+
intersection_end = self.translate_row_col_to_index(row, col2)
|
| 991 |
+
|
| 992 |
+
_, from_column = self.translate_index_to_position(intersection_start)
|
| 993 |
+
_, to_column = self.translate_index_to_position(intersection_end)
|
| 994 |
+
|
| 995 |
+
# In Vi mode, the upper boundary is always included. For Emacs
|
| 996 |
+
# mode, that's not the case.
|
| 997 |
+
if vi_mode():
|
| 998 |
+
to_column += 1
|
| 999 |
+
|
| 1000 |
+
return from_column, to_column
|
| 1001 |
+
return None
|
| 1002 |
+
|
| 1003 |
+
def cut_selection(self) -> tuple[Document, ClipboardData]:
|
| 1004 |
+
"""
|
| 1005 |
+
Return a (:class:`.Document`, :class:`.ClipboardData`) tuple, where the
|
| 1006 |
+
document represents the new document when the selection is cut, and the
|
| 1007 |
+
clipboard data, represents whatever has to be put on the clipboard.
|
| 1008 |
+
"""
|
| 1009 |
+
if self.selection:
|
| 1010 |
+
cut_parts = []
|
| 1011 |
+
remaining_parts = []
|
| 1012 |
+
new_cursor_position = self.cursor_position
|
| 1013 |
+
|
| 1014 |
+
last_to = 0
|
| 1015 |
+
for from_, to in self.selection_ranges():
|
| 1016 |
+
if last_to == 0:
|
| 1017 |
+
new_cursor_position = from_
|
| 1018 |
+
|
| 1019 |
+
remaining_parts.append(self.text[last_to:from_])
|
| 1020 |
+
cut_parts.append(self.text[from_:to])
|
| 1021 |
+
last_to = to
|
| 1022 |
+
|
| 1023 |
+
remaining_parts.append(self.text[last_to:])
|
| 1024 |
+
|
| 1025 |
+
cut_text = "\n".join(cut_parts)
|
| 1026 |
+
remaining_text = "".join(remaining_parts)
|
| 1027 |
+
|
| 1028 |
+
# In case of a LINES selection, don't include the trailing newline.
|
| 1029 |
+
if self.selection.type == SelectionType.LINES and cut_text.endswith("\n"):
|
| 1030 |
+
cut_text = cut_text[:-1]
|
| 1031 |
+
|
| 1032 |
+
return (
|
| 1033 |
+
Document(text=remaining_text, cursor_position=new_cursor_position),
|
| 1034 |
+
ClipboardData(cut_text, self.selection.type),
|
| 1035 |
+
)
|
| 1036 |
+
else:
|
| 1037 |
+
return self, ClipboardData("")
|
| 1038 |
+
|
| 1039 |
+
def paste_clipboard_data(
|
| 1040 |
+
self,
|
| 1041 |
+
data: ClipboardData,
|
| 1042 |
+
paste_mode: PasteMode = PasteMode.EMACS,
|
| 1043 |
+
count: int = 1,
|
| 1044 |
+
) -> Document:
|
| 1045 |
+
"""
|
| 1046 |
+
Return a new :class:`.Document` instance which contains the result if
|
| 1047 |
+
we would paste this data at the current cursor position.
|
| 1048 |
+
|
| 1049 |
+
:param paste_mode: Where to paste. (Before/after/emacs.)
|
| 1050 |
+
:param count: When >1, Paste multiple times.
|
| 1051 |
+
"""
|
| 1052 |
+
before = paste_mode == PasteMode.VI_BEFORE
|
| 1053 |
+
after = paste_mode == PasteMode.VI_AFTER
|
| 1054 |
+
|
| 1055 |
+
if data.type == SelectionType.CHARACTERS:
|
| 1056 |
+
if after:
|
| 1057 |
+
new_text = (
|
| 1058 |
+
self.text[: self.cursor_position + 1]
|
| 1059 |
+
+ data.text * count
|
| 1060 |
+
+ self.text[self.cursor_position + 1 :]
|
| 1061 |
+
)
|
| 1062 |
+
else:
|
| 1063 |
+
new_text = (
|
| 1064 |
+
self.text_before_cursor + data.text * count + self.text_after_cursor
|
| 1065 |
+
)
|
| 1066 |
+
|
| 1067 |
+
new_cursor_position = self.cursor_position + len(data.text) * count
|
| 1068 |
+
if before:
|
| 1069 |
+
new_cursor_position -= 1
|
| 1070 |
+
|
| 1071 |
+
elif data.type == SelectionType.LINES:
|
| 1072 |
+
l = self.cursor_position_row
|
| 1073 |
+
if before:
|
| 1074 |
+
lines = self.lines[:l] + [data.text] * count + self.lines[l:]
|
| 1075 |
+
new_text = "\n".join(lines)
|
| 1076 |
+
new_cursor_position = len("".join(self.lines[:l])) + l
|
| 1077 |
+
else:
|
| 1078 |
+
lines = self.lines[: l + 1] + [data.text] * count + self.lines[l + 1 :]
|
| 1079 |
+
new_cursor_position = len("".join(self.lines[: l + 1])) + l + 1
|
| 1080 |
+
new_text = "\n".join(lines)
|
| 1081 |
+
|
| 1082 |
+
elif data.type == SelectionType.BLOCK:
|
| 1083 |
+
lines = self.lines[:]
|
| 1084 |
+
start_line = self.cursor_position_row
|
| 1085 |
+
start_column = self.cursor_position_col + (0 if before else 1)
|
| 1086 |
+
|
| 1087 |
+
for i, line in enumerate(data.text.split("\n")):
|
| 1088 |
+
index = i + start_line
|
| 1089 |
+
if index >= len(lines):
|
| 1090 |
+
lines.append("")
|
| 1091 |
+
|
| 1092 |
+
lines[index] = lines[index].ljust(start_column)
|
| 1093 |
+
lines[index] = (
|
| 1094 |
+
lines[index][:start_column]
|
| 1095 |
+
+ line * count
|
| 1096 |
+
+ lines[index][start_column:]
|
| 1097 |
+
)
|
| 1098 |
+
|
| 1099 |
+
new_text = "\n".join(lines)
|
| 1100 |
+
new_cursor_position = self.cursor_position + (0 if before else 1)
|
| 1101 |
+
|
| 1102 |
+
return Document(text=new_text, cursor_position=new_cursor_position)
|
| 1103 |
+
|
| 1104 |
+
def empty_line_count_at_the_end(self) -> int:
|
| 1105 |
+
"""
|
| 1106 |
+
Return number of empty lines at the end of the document.
|
| 1107 |
+
"""
|
| 1108 |
+
count = 0
|
| 1109 |
+
for line in self.lines[::-1]:
|
| 1110 |
+
if not line or line.isspace():
|
| 1111 |
+
count += 1
|
| 1112 |
+
else:
|
| 1113 |
+
break
|
| 1114 |
+
|
| 1115 |
+
return count
|
| 1116 |
+
|
| 1117 |
+
def start_of_paragraph(self, count: int = 1, before: bool = False) -> int:
|
| 1118 |
+
"""
|
| 1119 |
+
Return the start of the current paragraph. (Relative cursor position.)
|
| 1120 |
+
"""
|
| 1121 |
+
|
| 1122 |
+
def match_func(text: str) -> bool:
|
| 1123 |
+
return not text or text.isspace()
|
| 1124 |
+
|
| 1125 |
+
line_index = self.find_previous_matching_line(
|
| 1126 |
+
match_func=match_func, count=count
|
| 1127 |
+
)
|
| 1128 |
+
|
| 1129 |
+
if line_index:
|
| 1130 |
+
add = 0 if before else 1
|
| 1131 |
+
return min(0, self.get_cursor_up_position(count=-line_index) + add)
|
| 1132 |
+
else:
|
| 1133 |
+
return -self.cursor_position
|
| 1134 |
+
|
| 1135 |
+
def end_of_paragraph(self, count: int = 1, after: bool = False) -> int:
|
| 1136 |
+
"""
|
| 1137 |
+
Return the end of the current paragraph. (Relative cursor position.)
|
| 1138 |
+
"""
|
| 1139 |
+
|
| 1140 |
+
def match_func(text: str) -> bool:
|
| 1141 |
+
return not text or text.isspace()
|
| 1142 |
+
|
| 1143 |
+
line_index = self.find_next_matching_line(match_func=match_func, count=count)
|
| 1144 |
+
|
| 1145 |
+
if line_index:
|
| 1146 |
+
add = 0 if after else 1
|
| 1147 |
+
return max(0, self.get_cursor_down_position(count=line_index) - add)
|
| 1148 |
+
else:
|
| 1149 |
+
return len(self.text_after_cursor)
|
| 1150 |
+
|
| 1151 |
+
# Modifiers.
|
| 1152 |
+
|
| 1153 |
+
def insert_after(self, text: str) -> Document:
|
| 1154 |
+
"""
|
| 1155 |
+
Create a new document, with this text inserted after the buffer.
|
| 1156 |
+
It keeps selection ranges and cursor position in sync.
|
| 1157 |
+
"""
|
| 1158 |
+
return Document(
|
| 1159 |
+
text=self.text + text,
|
| 1160 |
+
cursor_position=self.cursor_position,
|
| 1161 |
+
selection=self.selection,
|
| 1162 |
+
)
|
| 1163 |
+
|
| 1164 |
+
def insert_before(self, text: str) -> Document:
|
| 1165 |
+
"""
|
| 1166 |
+
Create a new document, with this text inserted before the buffer.
|
| 1167 |
+
It keeps selection ranges and cursor position in sync.
|
| 1168 |
+
"""
|
| 1169 |
+
selection_state = self.selection
|
| 1170 |
+
|
| 1171 |
+
if selection_state:
|
| 1172 |
+
selection_state = SelectionState(
|
| 1173 |
+
original_cursor_position=selection_state.original_cursor_position
|
| 1174 |
+
+ len(text),
|
| 1175 |
+
type=selection_state.type,
|
| 1176 |
+
)
|
| 1177 |
+
|
| 1178 |
+
return Document(
|
| 1179 |
+
text=text + self.text,
|
| 1180 |
+
cursor_position=self.cursor_position + len(text),
|
| 1181 |
+
selection=selection_state,
|
| 1182 |
+
)
|
lib/python3.12/site-packages/prompt_toolkit/enums.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from enum import Enum
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class EditingMode(Enum):
|
| 7 |
+
# The set of key bindings that is active.
|
| 8 |
+
VI = "VI"
|
| 9 |
+
EMACS = "EMACS"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
#: Name of the search buffer.
|
| 13 |
+
SEARCH_BUFFER = "SEARCH_BUFFER"
|
| 14 |
+
|
| 15 |
+
#: Name of the default buffer.
|
| 16 |
+
DEFAULT_BUFFER = "DEFAULT_BUFFER"
|
| 17 |
+
|
| 18 |
+
#: Name of the system buffer.
|
| 19 |
+
SYSTEM_BUFFER = "SYSTEM_BUFFER"
|
lib/python3.12/site-packages/prompt_toolkit/filters/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Filters decide whether something is active or not (they decide about a boolean
|
| 3 |
+
state). This is used to enable/disable features, like key bindings, parts of
|
| 4 |
+
the layout and other stuff. For instance, we could have a `HasSearch` filter
|
| 5 |
+
attached to some part of the layout, in order to show that part of the user
|
| 6 |
+
interface only while the user is searching.
|
| 7 |
+
|
| 8 |
+
Filters are made to avoid having to attach callbacks to all event in order to
|
| 9 |
+
propagate state. However, they are lazy, they don't automatically propagate the
|
| 10 |
+
state of what they are observing. Only when a filter is called (it's actually a
|
| 11 |
+
callable), it will calculate its value. So, its not really reactive
|
| 12 |
+
programming, but it's made to fit for this framework.
|
| 13 |
+
|
| 14 |
+
Filters can be chained using ``&`` and ``|`` operations, and inverted using the
|
| 15 |
+
``~`` operator, for instance::
|
| 16 |
+
|
| 17 |
+
filter = has_focus('default') & ~ has_selection
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
from .app import *
|
| 23 |
+
from .base import Always, Condition, Filter, FilterOrBool, Never
|
| 24 |
+
from .cli import *
|
| 25 |
+
from .utils import is_true, to_filter
|
| 26 |
+
|
| 27 |
+
__all__ = [
|
| 28 |
+
# app
|
| 29 |
+
"has_arg",
|
| 30 |
+
"has_completions",
|
| 31 |
+
"completion_is_selected",
|
| 32 |
+
"has_focus",
|
| 33 |
+
"buffer_has_focus",
|
| 34 |
+
"has_selection",
|
| 35 |
+
"has_validation_error",
|
| 36 |
+
"is_done",
|
| 37 |
+
"is_read_only",
|
| 38 |
+
"is_multiline",
|
| 39 |
+
"renderer_height_is_known",
|
| 40 |
+
"in_editing_mode",
|
| 41 |
+
"in_paste_mode",
|
| 42 |
+
"vi_mode",
|
| 43 |
+
"vi_navigation_mode",
|
| 44 |
+
"vi_insert_mode",
|
| 45 |
+
"vi_insert_multiple_mode",
|
| 46 |
+
"vi_replace_mode",
|
| 47 |
+
"vi_selection_mode",
|
| 48 |
+
"vi_waiting_for_text_object_mode",
|
| 49 |
+
"vi_digraph_mode",
|
| 50 |
+
"vi_recording_macro",
|
| 51 |
+
"emacs_mode",
|
| 52 |
+
"emacs_insert_mode",
|
| 53 |
+
"emacs_selection_mode",
|
| 54 |
+
"shift_selection_mode",
|
| 55 |
+
"is_searching",
|
| 56 |
+
"control_is_searchable",
|
| 57 |
+
"vi_search_direction_reversed",
|
| 58 |
+
# base.
|
| 59 |
+
"Filter",
|
| 60 |
+
"Never",
|
| 61 |
+
"Always",
|
| 62 |
+
"Condition",
|
| 63 |
+
"FilterOrBool",
|
| 64 |
+
# utils.
|
| 65 |
+
"is_true",
|
| 66 |
+
"to_filter",
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
from .cli import __all__ as cli_all
|
| 70 |
+
|
| 71 |
+
__all__.extend(cli_all)
|
lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (2.05 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (15.8 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/base.cpython-312.pyc
ADDED
|
Binary file (10.8 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/cli.cpython-312.pyc
ADDED
|
Binary file (3.7 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/utils.cpython-312.pyc
ADDED
|
Binary file (1.33 kB). View file
|
|
|
lib/python3.12/site-packages/prompt_toolkit/filters/app.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Filters that accept a `Application` as argument.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from typing import TYPE_CHECKING
|
| 8 |
+
|
| 9 |
+
from prompt_toolkit.application.current import get_app
|
| 10 |
+
from prompt_toolkit.cache import memoized
|
| 11 |
+
from prompt_toolkit.enums import EditingMode
|
| 12 |
+
|
| 13 |
+
from .base import Condition
|
| 14 |
+
|
| 15 |
+
if TYPE_CHECKING:
|
| 16 |
+
from prompt_toolkit.layout.layout import FocusableElement
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
__all__ = [
|
| 20 |
+
"has_arg",
|
| 21 |
+
"has_completions",
|
| 22 |
+
"completion_is_selected",
|
| 23 |
+
"has_focus",
|
| 24 |
+
"buffer_has_focus",
|
| 25 |
+
"has_selection",
|
| 26 |
+
"has_suggestion",
|
| 27 |
+
"has_validation_error",
|
| 28 |
+
"is_done",
|
| 29 |
+
"is_read_only",
|
| 30 |
+
"is_multiline",
|
| 31 |
+
"renderer_height_is_known",
|
| 32 |
+
"in_editing_mode",
|
| 33 |
+
"in_paste_mode",
|
| 34 |
+
"vi_mode",
|
| 35 |
+
"vi_navigation_mode",
|
| 36 |
+
"vi_insert_mode",
|
| 37 |
+
"vi_insert_multiple_mode",
|
| 38 |
+
"vi_replace_mode",
|
| 39 |
+
"vi_selection_mode",
|
| 40 |
+
"vi_waiting_for_text_object_mode",
|
| 41 |
+
"vi_digraph_mode",
|
| 42 |
+
"vi_recording_macro",
|
| 43 |
+
"emacs_mode",
|
| 44 |
+
"emacs_insert_mode",
|
| 45 |
+
"emacs_selection_mode",
|
| 46 |
+
"shift_selection_mode",
|
| 47 |
+
"is_searching",
|
| 48 |
+
"control_is_searchable",
|
| 49 |
+
"vi_search_direction_reversed",
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# NOTE: `has_focus` below should *not* be `memoized`. It can reference any user
|
| 54 |
+
# control. For instance, if we would continuously create new
|
| 55 |
+
# `PromptSession` instances, then previous instances won't be released,
|
| 56 |
+
# because this memoize (which caches results in the global scope) will
|
| 57 |
+
# still refer to each instance.
|
| 58 |
+
def has_focus(value: FocusableElement) -> Condition:
|
| 59 |
+
"""
|
| 60 |
+
Enable when this buffer has the focus.
|
| 61 |
+
"""
|
| 62 |
+
from prompt_toolkit.buffer import Buffer
|
| 63 |
+
from prompt_toolkit.layout import walk
|
| 64 |
+
from prompt_toolkit.layout.containers import Window, to_container
|
| 65 |
+
from prompt_toolkit.layout.controls import UIControl
|
| 66 |
+
|
| 67 |
+
if isinstance(value, str):
|
| 68 |
+
|
| 69 |
+
def test() -> bool:
|
| 70 |
+
return get_app().current_buffer.name == value
|
| 71 |
+
|
| 72 |
+
elif isinstance(value, Buffer):
|
| 73 |
+
|
| 74 |
+
def test() -> bool:
|
| 75 |
+
return get_app().current_buffer == value
|
| 76 |
+
|
| 77 |
+
elif isinstance(value, UIControl):
|
| 78 |
+
|
| 79 |
+
def test() -> bool:
|
| 80 |
+
return get_app().layout.current_control == value
|
| 81 |
+
|
| 82 |
+
else:
|
| 83 |
+
value = to_container(value)
|
| 84 |
+
|
| 85 |
+
if isinstance(value, Window):
|
| 86 |
+
|
| 87 |
+
def test() -> bool:
|
| 88 |
+
return get_app().layout.current_window == value
|
| 89 |
+
|
| 90 |
+
else:
|
| 91 |
+
|
| 92 |
+
def test() -> bool:
|
| 93 |
+
# Consider focused when any window inside this container is
|
| 94 |
+
# focused.
|
| 95 |
+
current_window = get_app().layout.current_window
|
| 96 |
+
|
| 97 |
+
for c in walk(value):
|
| 98 |
+
if isinstance(c, Window) and c == current_window:
|
| 99 |
+
return True
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
@Condition
|
| 103 |
+
def has_focus_filter() -> bool:
|
| 104 |
+
return test()
|
| 105 |
+
|
| 106 |
+
return has_focus_filter
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@Condition
|
| 110 |
+
def buffer_has_focus() -> bool:
|
| 111 |
+
"""
|
| 112 |
+
Enabled when the currently focused control is a `BufferControl`.
|
| 113 |
+
"""
|
| 114 |
+
return get_app().layout.buffer_has_focus
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
@Condition
|
| 118 |
+
def has_selection() -> bool:
|
| 119 |
+
"""
|
| 120 |
+
Enable when the current buffer has a selection.
|
| 121 |
+
"""
|
| 122 |
+
return bool(get_app().current_buffer.selection_state)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@Condition
|
| 126 |
+
def has_suggestion() -> bool:
|
| 127 |
+
"""
|
| 128 |
+
Enable when the current buffer has a suggestion.
|
| 129 |
+
"""
|
| 130 |
+
buffer = get_app().current_buffer
|
| 131 |
+
return buffer.suggestion is not None and buffer.suggestion.text != ""
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
@Condition
|
| 135 |
+
def has_completions() -> bool:
|
| 136 |
+
"""
|
| 137 |
+
Enable when the current buffer has completions.
|
| 138 |
+
"""
|
| 139 |
+
state = get_app().current_buffer.complete_state
|
| 140 |
+
return state is not None and len(state.completions) > 0
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@Condition
|
| 144 |
+
def completion_is_selected() -> bool:
|
| 145 |
+
"""
|
| 146 |
+
True when the user selected a completion.
|
| 147 |
+
"""
|
| 148 |
+
complete_state = get_app().current_buffer.complete_state
|
| 149 |
+
return complete_state is not None and complete_state.current_completion is not None
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
@Condition
|
| 153 |
+
def is_read_only() -> bool:
|
| 154 |
+
"""
|
| 155 |
+
True when the current buffer is read only.
|
| 156 |
+
"""
|
| 157 |
+
return get_app().current_buffer.read_only()
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@Condition
|
| 161 |
+
def is_multiline() -> bool:
|
| 162 |
+
"""
|
| 163 |
+
True when the current buffer has been marked as multiline.
|
| 164 |
+
"""
|
| 165 |
+
return get_app().current_buffer.multiline()
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
@Condition
|
| 169 |
+
def has_validation_error() -> bool:
|
| 170 |
+
"Current buffer has validation error."
|
| 171 |
+
return get_app().current_buffer.validation_error is not None
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@Condition
|
| 175 |
+
def has_arg() -> bool:
|
| 176 |
+
"Enable when the input processor has an 'arg'."
|
| 177 |
+
return get_app().key_processor.arg is not None
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
@Condition
|
| 181 |
+
def is_done() -> bool:
|
| 182 |
+
"""
|
| 183 |
+
True when the CLI is returning, aborting or exiting.
|
| 184 |
+
"""
|
| 185 |
+
return get_app().is_done
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@Condition
|
| 189 |
+
def renderer_height_is_known() -> bool:
|
| 190 |
+
"""
|
| 191 |
+
Only True when the renderer knows it's real height.
|
| 192 |
+
|
| 193 |
+
(On VT100 terminals, we have to wait for a CPR response, before we can be
|
| 194 |
+
sure of the available height between the cursor position and the bottom of
|
| 195 |
+
the terminal. And usually it's nicer to wait with drawing bottom toolbars
|
| 196 |
+
until we receive the height, in order to avoid flickering -- first drawing
|
| 197 |
+
somewhere in the middle, and then again at the bottom.)
|
| 198 |
+
"""
|
| 199 |
+
return get_app().renderer.height_is_known
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
@memoized()
|
| 203 |
+
def in_editing_mode(editing_mode: EditingMode) -> Condition:
|
| 204 |
+
"""
|
| 205 |
+
Check whether a given editing mode is active. (Vi or Emacs.)
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
@Condition
|
| 209 |
+
def in_editing_mode_filter() -> bool:
|
| 210 |
+
return get_app().editing_mode == editing_mode
|
| 211 |
+
|
| 212 |
+
return in_editing_mode_filter
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@Condition
|
| 216 |
+
def in_paste_mode() -> bool:
|
| 217 |
+
return get_app().paste_mode()
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
@Condition
|
| 221 |
+
def vi_mode() -> bool:
|
| 222 |
+
return get_app().editing_mode == EditingMode.VI
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@Condition
|
| 226 |
+
def vi_navigation_mode() -> bool:
|
| 227 |
+
"""
|
| 228 |
+
Active when the set for Vi navigation key bindings are active.
|
| 229 |
+
"""
|
| 230 |
+
from prompt_toolkit.key_binding.vi_state import InputMode
|
| 231 |
+
|
| 232 |
+
app = get_app()
|
| 233 |
+
|
| 234 |
+
if (
|
| 235 |
+
app.editing_mode != EditingMode.VI
|
| 236 |
+
or app.vi_state.operator_func
|
| 237 |
+
or app.vi_state.waiting_for_digraph
|
| 238 |
+
or app.current_buffer.selection_state
|
| 239 |
+
):
|
| 240 |
+
return False
|
| 241 |
+
|
| 242 |
+
return (
|
| 243 |
+
app.vi_state.input_mode == InputMode.NAVIGATION
|
| 244 |
+
or app.vi_state.temporary_navigation_mode
|
| 245 |
+
or app.current_buffer.read_only()
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
@Condition
|
| 250 |
+
def vi_insert_mode() -> bool:
|
| 251 |
+
from prompt_toolkit.key_binding.vi_state import InputMode
|
| 252 |
+
|
| 253 |
+
app = get_app()
|
| 254 |
+
|
| 255 |
+
if (
|
| 256 |
+
app.editing_mode != EditingMode.VI
|
| 257 |
+
or app.vi_state.operator_func
|
| 258 |
+
or app.vi_state.waiting_for_digraph
|
| 259 |
+
or app.current_buffer.selection_state
|
| 260 |
+
or app.vi_state.temporary_navigation_mode
|
| 261 |
+
or app.current_buffer.read_only()
|
| 262 |
+
):
|
| 263 |
+
return False
|
| 264 |
+
|
| 265 |
+
return app.vi_state.input_mode == InputMode.INSERT
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@Condition
|
| 269 |
+
def vi_insert_multiple_mode() -> bool:
|
| 270 |
+
from prompt_toolkit.key_binding.vi_state import InputMode
|
| 271 |
+
|
| 272 |
+
app = get_app()
|
| 273 |
+
|
| 274 |
+
if (
|
| 275 |
+
app.editing_mode != EditingMode.VI
|
| 276 |
+
or app.vi_state.operator_func
|
| 277 |
+
or app.vi_state.waiting_for_digraph
|
| 278 |
+
or app.current_buffer.selection_state
|
| 279 |
+
or app.vi_state.temporary_navigation_mode
|
| 280 |
+
or app.current_buffer.read_only()
|
| 281 |
+
):
|
| 282 |
+
return False
|
| 283 |
+
|
| 284 |
+
return app.vi_state.input_mode == InputMode.INSERT_MULTIPLE
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
@Condition
|
| 288 |
+
def vi_replace_mode() -> bool:
|
| 289 |
+
from prompt_toolkit.key_binding.vi_state import InputMode
|
| 290 |
+
|
| 291 |
+
app = get_app()
|
| 292 |
+
|
| 293 |
+
if (
|
| 294 |
+
app.editing_mode != EditingMode.VI
|
| 295 |
+
or app.vi_state.operator_func
|
| 296 |
+
or app.vi_state.waiting_for_digraph
|
| 297 |
+
or app.current_buffer.selection_state
|
| 298 |
+
or app.vi_state.temporary_navigation_mode
|
| 299 |
+
or app.current_buffer.read_only()
|
| 300 |
+
):
|
| 301 |
+
return False
|
| 302 |
+
|
| 303 |
+
return app.vi_state.input_mode == InputMode.REPLACE
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
@Condition
|
| 307 |
+
def vi_replace_single_mode() -> bool:
|
| 308 |
+
from prompt_toolkit.key_binding.vi_state import InputMode
|
| 309 |
+
|
| 310 |
+
app = get_app()
|
| 311 |
+
|
| 312 |
+
if (
|
| 313 |
+
app.editing_mode != EditingMode.VI
|
| 314 |
+
or app.vi_state.operator_func
|
| 315 |
+
or app.vi_state.waiting_for_digraph
|
| 316 |
+
or app.current_buffer.selection_state
|
| 317 |
+
or app.vi_state.temporary_navigation_mode
|
| 318 |
+
or app.current_buffer.read_only()
|
| 319 |
+
):
|
| 320 |
+
return False
|
| 321 |
+
|
| 322 |
+
return app.vi_state.input_mode == InputMode.REPLACE_SINGLE
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
@Condition
|
| 326 |
+
def vi_selection_mode() -> bool:
|
| 327 |
+
app = get_app()
|
| 328 |
+
if app.editing_mode != EditingMode.VI:
|
| 329 |
+
return False
|
| 330 |
+
|
| 331 |
+
return bool(app.current_buffer.selection_state)
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
@Condition
|
| 335 |
+
def vi_waiting_for_text_object_mode() -> bool:
|
| 336 |
+
app = get_app()
|
| 337 |
+
if app.editing_mode != EditingMode.VI:
|
| 338 |
+
return False
|
| 339 |
+
|
| 340 |
+
return app.vi_state.operator_func is not None
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
@Condition
|
| 344 |
+
def vi_digraph_mode() -> bool:
|
| 345 |
+
app = get_app()
|
| 346 |
+
if app.editing_mode != EditingMode.VI:
|
| 347 |
+
return False
|
| 348 |
+
|
| 349 |
+
return app.vi_state.waiting_for_digraph
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
@Condition
|
| 353 |
+
def vi_recording_macro() -> bool:
|
| 354 |
+
"When recording a Vi macro."
|
| 355 |
+
app = get_app()
|
| 356 |
+
if app.editing_mode != EditingMode.VI:
|
| 357 |
+
return False
|
| 358 |
+
|
| 359 |
+
return app.vi_state.recording_register is not None
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
@Condition
|
| 363 |
+
def emacs_mode() -> bool:
|
| 364 |
+
"When the Emacs bindings are active."
|
| 365 |
+
return get_app().editing_mode == EditingMode.EMACS
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
@Condition
|
| 369 |
+
def emacs_insert_mode() -> bool:
|
| 370 |
+
app = get_app()
|
| 371 |
+
if (
|
| 372 |
+
app.editing_mode != EditingMode.EMACS
|
| 373 |
+
or app.current_buffer.selection_state
|
| 374 |
+
or app.current_buffer.read_only()
|
| 375 |
+
):
|
| 376 |
+
return False
|
| 377 |
+
return True
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
@Condition
|
| 381 |
+
def emacs_selection_mode() -> bool:
|
| 382 |
+
app = get_app()
|
| 383 |
+
return bool(
|
| 384 |
+
app.editing_mode == EditingMode.EMACS and app.current_buffer.selection_state
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
@Condition
|
| 389 |
+
def shift_selection_mode() -> bool:
|
| 390 |
+
app = get_app()
|
| 391 |
+
return bool(
|
| 392 |
+
app.current_buffer.selection_state
|
| 393 |
+
and app.current_buffer.selection_state.shift_mode
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
@Condition
|
| 398 |
+
def is_searching() -> bool:
|
| 399 |
+
"When we are searching."
|
| 400 |
+
app = get_app()
|
| 401 |
+
return app.layout.is_searching
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
@Condition
|
| 405 |
+
def control_is_searchable() -> bool:
|
| 406 |
+
"When the current UIControl is searchable."
|
| 407 |
+
from prompt_toolkit.layout.controls import BufferControl
|
| 408 |
+
|
| 409 |
+
control = get_app().layout.current_control
|
| 410 |
+
|
| 411 |
+
return (
|
| 412 |
+
isinstance(control, BufferControl) and control.search_buffer_control is not None
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
@Condition
|
| 417 |
+
def vi_search_direction_reversed() -> bool:
|
| 418 |
+
"When the '/' and '?' key bindings for Vi-style searching have been reversed."
|
| 419 |
+
return get_app().reverse_vi_search_direction()
|
lib/python3.12/site-packages/prompt_toolkit/filters/base.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from abc import ABCMeta, abstractmethod
|
| 4 |
+
from typing import Callable, Iterable, Union
|
| 5 |
+
|
| 6 |
+
__all__ = ["Filter", "Never", "Always", "Condition", "FilterOrBool"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Filter(metaclass=ABCMeta):
|
| 10 |
+
"""
|
| 11 |
+
Base class for any filter to activate/deactivate a feature, depending on a
|
| 12 |
+
condition.
|
| 13 |
+
|
| 14 |
+
The return value of ``__call__`` will tell if the feature should be active.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(self) -> None:
|
| 18 |
+
self._and_cache: dict[Filter, Filter] = {}
|
| 19 |
+
self._or_cache: dict[Filter, Filter] = {}
|
| 20 |
+
self._invert_result: Filter | None = None
|
| 21 |
+
|
| 22 |
+
@abstractmethod
|
| 23 |
+
def __call__(self) -> bool:
|
| 24 |
+
"""
|
| 25 |
+
The actual call to evaluate the filter.
|
| 26 |
+
"""
|
| 27 |
+
return True
|
| 28 |
+
|
| 29 |
+
def __and__(self, other: Filter) -> Filter:
|
| 30 |
+
"""
|
| 31 |
+
Chaining of filters using the & operator.
|
| 32 |
+
"""
|
| 33 |
+
assert isinstance(other, Filter), f"Expecting filter, got {other!r}"
|
| 34 |
+
|
| 35 |
+
if isinstance(other, Always):
|
| 36 |
+
return self
|
| 37 |
+
if isinstance(other, Never):
|
| 38 |
+
return other
|
| 39 |
+
|
| 40 |
+
if other in self._and_cache:
|
| 41 |
+
return self._and_cache[other]
|
| 42 |
+
|
| 43 |
+
result = _AndList.create([self, other])
|
| 44 |
+
self._and_cache[other] = result
|
| 45 |
+
return result
|
| 46 |
+
|
| 47 |
+
def __or__(self, other: Filter) -> Filter:
|
| 48 |
+
"""
|
| 49 |
+
Chaining of filters using the | operator.
|
| 50 |
+
"""
|
| 51 |
+
assert isinstance(other, Filter), f"Expecting filter, got {other!r}"
|
| 52 |
+
|
| 53 |
+
if isinstance(other, Always):
|
| 54 |
+
return other
|
| 55 |
+
if isinstance(other, Never):
|
| 56 |
+
return self
|
| 57 |
+
|
| 58 |
+
if other in self._or_cache:
|
| 59 |
+
return self._or_cache[other]
|
| 60 |
+
|
| 61 |
+
result = _OrList.create([self, other])
|
| 62 |
+
self._or_cache[other] = result
|
| 63 |
+
return result
|
| 64 |
+
|
| 65 |
+
def __invert__(self) -> Filter:
|
| 66 |
+
"""
|
| 67 |
+
Inverting of filters using the ~ operator.
|
| 68 |
+
"""
|
| 69 |
+
if self._invert_result is None:
|
| 70 |
+
self._invert_result = _Invert(self)
|
| 71 |
+
|
| 72 |
+
return self._invert_result
|
| 73 |
+
|
| 74 |
+
def __bool__(self) -> None:
|
| 75 |
+
"""
|
| 76 |
+
By purpose, we don't allow bool(...) operations directly on a filter,
|
| 77 |
+
because the meaning is ambiguous.
|
| 78 |
+
|
| 79 |
+
Executing a filter has to be done always by calling it. Providing
|
| 80 |
+
defaults for `None` values should be done through an `is None` check
|
| 81 |
+
instead of for instance ``filter1 or Always()``.
|
| 82 |
+
"""
|
| 83 |
+
raise ValueError(
|
| 84 |
+
"The truth value of a Filter is ambiguous. Instead, call it as a function."
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _remove_duplicates(filters: list[Filter]) -> list[Filter]:
|
| 89 |
+
result = []
|
| 90 |
+
for f in filters:
|
| 91 |
+
if f not in result:
|
| 92 |
+
result.append(f)
|
| 93 |
+
return result
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class _AndList(Filter):
|
| 97 |
+
"""
|
| 98 |
+
Result of &-operation between several filters.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __init__(self, filters: list[Filter]) -> None:
|
| 102 |
+
super().__init__()
|
| 103 |
+
self.filters = filters
|
| 104 |
+
|
| 105 |
+
@classmethod
|
| 106 |
+
def create(cls, filters: Iterable[Filter]) -> Filter:
|
| 107 |
+
"""
|
| 108 |
+
Create a new filter by applying an `&` operator between them.
|
| 109 |
+
|
| 110 |
+
If there's only one unique filter in the given iterable, it will return
|
| 111 |
+
that one filter instead of an `_AndList`.
|
| 112 |
+
"""
|
| 113 |
+
filters_2: list[Filter] = []
|
| 114 |
+
|
| 115 |
+
for f in filters:
|
| 116 |
+
if isinstance(f, _AndList): # Turn nested _AndLists into one.
|
| 117 |
+
filters_2.extend(f.filters)
|
| 118 |
+
else:
|
| 119 |
+
filters_2.append(f)
|
| 120 |
+
|
| 121 |
+
# Remove duplicates. This could speed up execution, and doesn't make a
|
| 122 |
+
# difference for the evaluation.
|
| 123 |
+
filters = _remove_duplicates(filters_2)
|
| 124 |
+
|
| 125 |
+
# If only one filter is left, return that without wrapping into an
|
| 126 |
+
# `_AndList`.
|
| 127 |
+
if len(filters) == 1:
|
| 128 |
+
return filters[0]
|
| 129 |
+
|
| 130 |
+
return cls(filters)
|
| 131 |
+
|
| 132 |
+
def __call__(self) -> bool:
|
| 133 |
+
return all(f() for f in self.filters)
|
| 134 |
+
|
| 135 |
+
def __repr__(self) -> str:
|
| 136 |
+
return "&".join(repr(f) for f in self.filters)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class _OrList(Filter):
|
| 140 |
+
"""
|
| 141 |
+
Result of |-operation between several filters.
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
def __init__(self, filters: list[Filter]) -> None:
|
| 145 |
+
super().__init__()
|
| 146 |
+
self.filters = filters
|
| 147 |
+
|
| 148 |
+
@classmethod
|
| 149 |
+
def create(cls, filters: Iterable[Filter]) -> Filter:
|
| 150 |
+
"""
|
| 151 |
+
Create a new filter by applying an `|` operator between them.
|
| 152 |
+
|
| 153 |
+
If there's only one unique filter in the given iterable, it will return
|
| 154 |
+
that one filter instead of an `_OrList`.
|
| 155 |
+
"""
|
| 156 |
+
filters_2: list[Filter] = []
|
| 157 |
+
|
| 158 |
+
for f in filters:
|
| 159 |
+
if isinstance(f, _OrList): # Turn nested _AndLists into one.
|
| 160 |
+
filters_2.extend(f.filters)
|
| 161 |
+
else:
|
| 162 |
+
filters_2.append(f)
|
| 163 |
+
|
| 164 |
+
# Remove duplicates. This could speed up execution, and doesn't make a
|
| 165 |
+
# difference for the evaluation.
|
| 166 |
+
filters = _remove_duplicates(filters_2)
|
| 167 |
+
|
| 168 |
+
# If only one filter is left, return that without wrapping into an
|
| 169 |
+
# `_AndList`.
|
| 170 |
+
if len(filters) == 1:
|
| 171 |
+
return filters[0]
|
| 172 |
+
|
| 173 |
+
return cls(filters)
|
| 174 |
+
|
| 175 |
+
def __call__(self) -> bool:
|
| 176 |
+
return any(f() for f in self.filters)
|
| 177 |
+
|
| 178 |
+
def __repr__(self) -> str:
|
| 179 |
+
return "|".join(repr(f) for f in self.filters)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class _Invert(Filter):
|
| 183 |
+
"""
|
| 184 |
+
Negation of another filter.
|
| 185 |
+
"""
|
| 186 |
+
|
| 187 |
+
def __init__(self, filter: Filter) -> None:
|
| 188 |
+
super().__init__()
|
| 189 |
+
self.filter = filter
|
| 190 |
+
|
| 191 |
+
def __call__(self) -> bool:
|
| 192 |
+
return not self.filter()
|
| 193 |
+
|
| 194 |
+
def __repr__(self) -> str:
|
| 195 |
+
return f"~{self.filter!r}"
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class Always(Filter):
|
| 199 |
+
"""
|
| 200 |
+
Always enable feature.
|
| 201 |
+
"""
|
| 202 |
+
|
| 203 |
+
def __call__(self) -> bool:
|
| 204 |
+
return True
|
| 205 |
+
|
| 206 |
+
def __or__(self, other: Filter) -> Filter:
|
| 207 |
+
return self
|
| 208 |
+
|
| 209 |
+
def __and__(self, other: Filter) -> Filter:
|
| 210 |
+
return other
|
| 211 |
+
|
| 212 |
+
def __invert__(self) -> Never:
|
| 213 |
+
return Never()
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class Never(Filter):
|
| 217 |
+
"""
|
| 218 |
+
Never enable feature.
|
| 219 |
+
"""
|
| 220 |
+
|
| 221 |
+
def __call__(self) -> bool:
|
| 222 |
+
return False
|
| 223 |
+
|
| 224 |
+
def __and__(self, other: Filter) -> Filter:
|
| 225 |
+
return self
|
| 226 |
+
|
| 227 |
+
def __or__(self, other: Filter) -> Filter:
|
| 228 |
+
return other
|
| 229 |
+
|
| 230 |
+
def __invert__(self) -> Always:
|
| 231 |
+
return Always()
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
class Condition(Filter):
|
| 235 |
+
"""
|
| 236 |
+
Turn any callable into a Filter. The callable is supposed to not take any
|
| 237 |
+
arguments.
|
| 238 |
+
|
| 239 |
+
This can be used as a decorator::
|
| 240 |
+
|
| 241 |
+
@Condition
|
| 242 |
+
def feature_is_active(): # `feature_is_active` becomes a Filter.
|
| 243 |
+
return True
|
| 244 |
+
|
| 245 |
+
:param func: Callable which takes no inputs and returns a boolean.
|
| 246 |
+
"""
|
| 247 |
+
|
| 248 |
+
def __init__(self, func: Callable[[], bool]) -> None:
|
| 249 |
+
super().__init__()
|
| 250 |
+
self.func = func
|
| 251 |
+
|
| 252 |
+
def __call__(self) -> bool:
|
| 253 |
+
return self.func()
|
| 254 |
+
|
| 255 |
+
def __repr__(self) -> str:
|
| 256 |
+
return f"Condition({self.func!r})"
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# Often used as type annotation.
|
| 260 |
+
FilterOrBool = Union[Filter, bool]
|
lib/python3.12/site-packages/prompt_toolkit/filters/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
For backwards-compatibility. keep this file.
|
| 3 |
+
(Many people are going to have key bindings that rely on this file.)
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from .app import *
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
# Old names.
|
| 12 |
+
"HasArg",
|
| 13 |
+
"HasCompletions",
|
| 14 |
+
"HasFocus",
|
| 15 |
+
"HasSelection",
|
| 16 |
+
"HasValidationError",
|
| 17 |
+
"IsDone",
|
| 18 |
+
"IsReadOnly",
|
| 19 |
+
"IsMultiline",
|
| 20 |
+
"RendererHeightIsKnown",
|
| 21 |
+
"InEditingMode",
|
| 22 |
+
"InPasteMode",
|
| 23 |
+
"ViMode",
|
| 24 |
+
"ViNavigationMode",
|
| 25 |
+
"ViInsertMode",
|
| 26 |
+
"ViInsertMultipleMode",
|
| 27 |
+
"ViReplaceMode",
|
| 28 |
+
"ViSelectionMode",
|
| 29 |
+
"ViWaitingForTextObjectMode",
|
| 30 |
+
"ViDigraphMode",
|
| 31 |
+
"EmacsMode",
|
| 32 |
+
"EmacsInsertMode",
|
| 33 |
+
"EmacsSelectionMode",
|
| 34 |
+
"IsSearching",
|
| 35 |
+
"HasSearch",
|
| 36 |
+
"ControlIsSearchable",
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
# Keep the original classnames for backwards compatibility.
|
| 40 |
+
HasValidationError = lambda: has_validation_error
|
| 41 |
+
HasArg = lambda: has_arg
|
| 42 |
+
IsDone = lambda: is_done
|
| 43 |
+
RendererHeightIsKnown = lambda: renderer_height_is_known
|
| 44 |
+
ViNavigationMode = lambda: vi_navigation_mode
|
| 45 |
+
InPasteMode = lambda: in_paste_mode
|
| 46 |
+
EmacsMode = lambda: emacs_mode
|
| 47 |
+
EmacsInsertMode = lambda: emacs_insert_mode
|
| 48 |
+
ViMode = lambda: vi_mode
|
| 49 |
+
IsSearching = lambda: is_searching
|
| 50 |
+
HasSearch = lambda: is_searching
|
| 51 |
+
ControlIsSearchable = lambda: control_is_searchable
|
| 52 |
+
EmacsSelectionMode = lambda: emacs_selection_mode
|
| 53 |
+
ViDigraphMode = lambda: vi_digraph_mode
|
| 54 |
+
ViWaitingForTextObjectMode = lambda: vi_waiting_for_text_object_mode
|
| 55 |
+
ViSelectionMode = lambda: vi_selection_mode
|
| 56 |
+
ViReplaceMode = lambda: vi_replace_mode
|
| 57 |
+
ViInsertMultipleMode = lambda: vi_insert_multiple_mode
|
| 58 |
+
ViInsertMode = lambda: vi_insert_mode
|
| 59 |
+
HasSelection = lambda: has_selection
|
| 60 |
+
HasCompletions = lambda: has_completions
|
| 61 |
+
IsReadOnly = lambda: is_read_only
|
| 62 |
+
IsMultiline = lambda: is_multiline
|
| 63 |
+
|
| 64 |
+
HasFocus = has_focus # No lambda here! (Has_focus is callable that returns a callable.)
|
| 65 |
+
InEditingMode = in_editing_mode
|
lib/python3.12/site-packages/prompt_toolkit/filters/utils.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .base import Always, Filter, FilterOrBool, Never
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"to_filter",
|
| 7 |
+
"is_true",
|
| 8 |
+
]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
_always = Always()
|
| 12 |
+
_never = Never()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
_bool_to_filter: dict[bool, Filter] = {
|
| 16 |
+
True: _always,
|
| 17 |
+
False: _never,
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def to_filter(bool_or_filter: FilterOrBool) -> Filter:
|
| 22 |
+
"""
|
| 23 |
+
Accept both booleans and Filters as input and
|
| 24 |
+
turn it into a Filter.
|
| 25 |
+
"""
|
| 26 |
+
if isinstance(bool_or_filter, bool):
|
| 27 |
+
return _bool_to_filter[bool_or_filter]
|
| 28 |
+
|
| 29 |
+
if isinstance(bool_or_filter, Filter):
|
| 30 |
+
return bool_or_filter
|
| 31 |
+
|
| 32 |
+
raise TypeError(f"Expecting a bool or a Filter instance. Got {bool_or_filter!r}")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def is_true(value: FilterOrBool) -> bool:
|
| 36 |
+
"""
|
| 37 |
+
Test whether `value` is True. In case of a Filter, call it.
|
| 38 |
+
|
| 39 |
+
:param value: Boolean or `Filter` instance.
|
| 40 |
+
"""
|
| 41 |
+
return to_filter(value)()
|
lib/python3.12/site-packages/prompt_toolkit/history.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Implementations for the history of a `Buffer`.
|
| 3 |
+
|
| 4 |
+
NOTE: There is no `DynamicHistory`:
|
| 5 |
+
This doesn't work well, because the `Buffer` needs to be able to attach
|
| 6 |
+
an event handler to the event when a history entry is loaded. This
|
| 7 |
+
loading can be done asynchronously and making the history swappable would
|
| 8 |
+
probably break this.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import datetime
|
| 14 |
+
import os
|
| 15 |
+
import threading
|
| 16 |
+
from abc import ABCMeta, abstractmethod
|
| 17 |
+
from asyncio import get_running_loop
|
| 18 |
+
from typing import AsyncGenerator, Iterable, Sequence, Union
|
| 19 |
+
|
| 20 |
+
__all__ = [
|
| 21 |
+
"History",
|
| 22 |
+
"ThreadedHistory",
|
| 23 |
+
"DummyHistory",
|
| 24 |
+
"FileHistory",
|
| 25 |
+
"InMemoryHistory",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class History(metaclass=ABCMeta):
|
| 30 |
+
"""
|
| 31 |
+
Base ``History`` class.
|
| 32 |
+
|
| 33 |
+
This also includes abstract methods for loading/storing history.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(self) -> None:
|
| 37 |
+
# In memory storage for strings.
|
| 38 |
+
self._loaded = False
|
| 39 |
+
|
| 40 |
+
# History that's loaded already, in reverse order. Latest, most recent
|
| 41 |
+
# item first.
|
| 42 |
+
self._loaded_strings: list[str] = []
|
| 43 |
+
|
| 44 |
+
#
|
| 45 |
+
# Methods expected by `Buffer`.
|
| 46 |
+
#
|
| 47 |
+
|
| 48 |
+
async def load(self) -> AsyncGenerator[str, None]:
|
| 49 |
+
"""
|
| 50 |
+
Load the history and yield all the entries in reverse order (latest,
|
| 51 |
+
most recent history entry first).
|
| 52 |
+
|
| 53 |
+
This method can be called multiple times from the `Buffer` to
|
| 54 |
+
repopulate the history when prompting for a new input. So we are
|
| 55 |
+
responsible here for both caching, and making sure that strings that
|
| 56 |
+
were were appended to the history will be incorporated next time this
|
| 57 |
+
method is called.
|
| 58 |
+
"""
|
| 59 |
+
if not self._loaded:
|
| 60 |
+
self._loaded_strings = list(self.load_history_strings())
|
| 61 |
+
self._loaded = True
|
| 62 |
+
|
| 63 |
+
for item in self._loaded_strings:
|
| 64 |
+
yield item
|
| 65 |
+
|
| 66 |
+
def get_strings(self) -> list[str]:
|
| 67 |
+
"""
|
| 68 |
+
Get the strings from the history that are loaded so far.
|
| 69 |
+
(In order. Oldest item first.)
|
| 70 |
+
"""
|
| 71 |
+
return self._loaded_strings[::-1]
|
| 72 |
+
|
| 73 |
+
def append_string(self, string: str) -> None:
|
| 74 |
+
"Add string to the history."
|
| 75 |
+
self._loaded_strings.insert(0, string)
|
| 76 |
+
self.store_string(string)
|
| 77 |
+
|
| 78 |
+
#
|
| 79 |
+
# Implementation for specific backends.
|
| 80 |
+
#
|
| 81 |
+
|
| 82 |
+
@abstractmethod
|
| 83 |
+
def load_history_strings(self) -> Iterable[str]:
|
| 84 |
+
"""
|
| 85 |
+
This should be a generator that yields `str` instances.
|
| 86 |
+
|
| 87 |
+
It should yield the most recent items first, because they are the most
|
| 88 |
+
important. (The history can already be used, even when it's only
|
| 89 |
+
partially loaded.)
|
| 90 |
+
"""
|
| 91 |
+
while False:
|
| 92 |
+
yield
|
| 93 |
+
|
| 94 |
+
@abstractmethod
|
| 95 |
+
def store_string(self, string: str) -> None:
|
| 96 |
+
"""
|
| 97 |
+
Store the string in persistent storage.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class ThreadedHistory(History):
|
| 102 |
+
"""
|
| 103 |
+
Wrapper around `History` implementations that run the `load()` generator in
|
| 104 |
+
a thread.
|
| 105 |
+
|
| 106 |
+
Use this to increase the start-up time of prompt_toolkit applications.
|
| 107 |
+
History entries are available as soon as they are loaded. We don't have to
|
| 108 |
+
wait for everything to be loaded.
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
def __init__(self, history: History) -> None:
|
| 112 |
+
super().__init__()
|
| 113 |
+
|
| 114 |
+
self.history = history
|
| 115 |
+
|
| 116 |
+
self._load_thread: threading.Thread | None = None
|
| 117 |
+
|
| 118 |
+
# Lock for accessing/manipulating `_loaded_strings` and `_loaded`
|
| 119 |
+
# together in a consistent state.
|
| 120 |
+
self._lock = threading.Lock()
|
| 121 |
+
|
| 122 |
+
# Events created by each `load()` call. Used to wait for new history
|
| 123 |
+
# entries from the loader thread.
|
| 124 |
+
self._string_load_events: list[threading.Event] = []
|
| 125 |
+
|
| 126 |
+
async def load(self) -> AsyncGenerator[str, None]:
|
| 127 |
+
"""
|
| 128 |
+
Like `History.load(), but call `self.load_history_strings()` in a
|
| 129 |
+
background thread.
|
| 130 |
+
"""
|
| 131 |
+
# Start the load thread, if this is called for the first time.
|
| 132 |
+
if not self._load_thread:
|
| 133 |
+
self._load_thread = threading.Thread(
|
| 134 |
+
target=self._in_load_thread,
|
| 135 |
+
daemon=True,
|
| 136 |
+
)
|
| 137 |
+
self._load_thread.start()
|
| 138 |
+
|
| 139 |
+
# Consume the `_loaded_strings` list, using asyncio.
|
| 140 |
+
loop = get_running_loop()
|
| 141 |
+
|
| 142 |
+
# Create threading Event so that we can wait for new items.
|
| 143 |
+
event = threading.Event()
|
| 144 |
+
event.set()
|
| 145 |
+
self._string_load_events.append(event)
|
| 146 |
+
|
| 147 |
+
items_yielded = 0
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
while True:
|
| 151 |
+
# Wait for new items to be available.
|
| 152 |
+
# (Use a timeout, because the executor thread is not a daemon
|
| 153 |
+
# thread. The "slow-history.py" example would otherwise hang if
|
| 154 |
+
# Control-C is pressed before the history is fully loaded,
|
| 155 |
+
# because there's still this non-daemon executor thread waiting
|
| 156 |
+
# for this event.)
|
| 157 |
+
got_timeout = await loop.run_in_executor(
|
| 158 |
+
None, lambda: event.wait(timeout=0.5)
|
| 159 |
+
)
|
| 160 |
+
if not got_timeout:
|
| 161 |
+
continue
|
| 162 |
+
|
| 163 |
+
# Read new items (in lock).
|
| 164 |
+
def in_executor() -> tuple[list[str], bool]:
|
| 165 |
+
with self._lock:
|
| 166 |
+
new_items = self._loaded_strings[items_yielded:]
|
| 167 |
+
done = self._loaded
|
| 168 |
+
event.clear()
|
| 169 |
+
return new_items, done
|
| 170 |
+
|
| 171 |
+
new_items, done = await loop.run_in_executor(None, in_executor)
|
| 172 |
+
|
| 173 |
+
items_yielded += len(new_items)
|
| 174 |
+
|
| 175 |
+
for item in new_items:
|
| 176 |
+
yield item
|
| 177 |
+
|
| 178 |
+
if done:
|
| 179 |
+
break
|
| 180 |
+
finally:
|
| 181 |
+
self._string_load_events.remove(event)
|
| 182 |
+
|
| 183 |
+
def _in_load_thread(self) -> None:
|
| 184 |
+
try:
|
| 185 |
+
# Start with an empty list. In case `append_string()` was called
|
| 186 |
+
# before `load()` happened. Then `.store_string()` will have
|
| 187 |
+
# written these entries back to disk and we will reload it.
|
| 188 |
+
self._loaded_strings = []
|
| 189 |
+
|
| 190 |
+
for item in self.history.load_history_strings():
|
| 191 |
+
with self._lock:
|
| 192 |
+
self._loaded_strings.append(item)
|
| 193 |
+
|
| 194 |
+
for event in self._string_load_events:
|
| 195 |
+
event.set()
|
| 196 |
+
finally:
|
| 197 |
+
with self._lock:
|
| 198 |
+
self._loaded = True
|
| 199 |
+
for event in self._string_load_events:
|
| 200 |
+
event.set()
|
| 201 |
+
|
| 202 |
+
def append_string(self, string: str) -> None:
|
| 203 |
+
with self._lock:
|
| 204 |
+
self._loaded_strings.insert(0, string)
|
| 205 |
+
self.store_string(string)
|
| 206 |
+
|
| 207 |
+
# All of the following are proxied to `self.history`.
|
| 208 |
+
|
| 209 |
+
def load_history_strings(self) -> Iterable[str]:
|
| 210 |
+
return self.history.load_history_strings()
|
| 211 |
+
|
| 212 |
+
def store_string(self, string: str) -> None:
|
| 213 |
+
self.history.store_string(string)
|
| 214 |
+
|
| 215 |
+
def __repr__(self) -> str:
|
| 216 |
+
return f"ThreadedHistory({self.history!r})"
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class InMemoryHistory(History):
|
| 220 |
+
"""
|
| 221 |
+
:class:`.History` class that keeps a list of all strings in memory.
|
| 222 |
+
|
| 223 |
+
In order to prepopulate the history, it's possible to call either
|
| 224 |
+
`append_string` for all items or pass a list of strings to `__init__` here.
|
| 225 |
+
"""
|
| 226 |
+
|
| 227 |
+
def __init__(self, history_strings: Sequence[str] | None = None) -> None:
|
| 228 |
+
super().__init__()
|
| 229 |
+
# Emulating disk storage.
|
| 230 |
+
if history_strings is None:
|
| 231 |
+
self._storage = []
|
| 232 |
+
else:
|
| 233 |
+
self._storage = list(history_strings)
|
| 234 |
+
|
| 235 |
+
def load_history_strings(self) -> Iterable[str]:
|
| 236 |
+
yield from self._storage[::-1]
|
| 237 |
+
|
| 238 |
+
def store_string(self, string: str) -> None:
|
| 239 |
+
self._storage.append(string)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
class DummyHistory(History):
|
| 243 |
+
"""
|
| 244 |
+
:class:`.History` object that doesn't remember anything.
|
| 245 |
+
"""
|
| 246 |
+
|
| 247 |
+
def load_history_strings(self) -> Iterable[str]:
|
| 248 |
+
return []
|
| 249 |
+
|
| 250 |
+
def store_string(self, string: str) -> None:
|
| 251 |
+
pass
|
| 252 |
+
|
| 253 |
+
def append_string(self, string: str) -> None:
|
| 254 |
+
# Don't remember this.
|
| 255 |
+
pass
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
_StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
class FileHistory(History):
|
| 262 |
+
"""
|
| 263 |
+
:class:`.History` class that stores all strings in a file.
|
| 264 |
+
"""
|
| 265 |
+
|
| 266 |
+
def __init__(self, filename: _StrOrBytesPath) -> None:
|
| 267 |
+
self.filename = filename
|
| 268 |
+
super().__init__()
|
| 269 |
+
|
| 270 |
+
def load_history_strings(self) -> Iterable[str]:
|
| 271 |
+
strings: list[str] = []
|
| 272 |
+
lines: list[str] = []
|
| 273 |
+
|
| 274 |
+
def add() -> None:
|
| 275 |
+
if lines:
|
| 276 |
+
# Join and drop trailing newline.
|
| 277 |
+
string = "".join(lines)[:-1]
|
| 278 |
+
|
| 279 |
+
strings.append(string)
|
| 280 |
+
|
| 281 |
+
if os.path.exists(self.filename):
|
| 282 |
+
with open(self.filename, "rb") as f:
|
| 283 |
+
for line_bytes in f:
|
| 284 |
+
line = line_bytes.decode("utf-8", errors="replace")
|
| 285 |
+
|
| 286 |
+
if line.startswith("+"):
|
| 287 |
+
lines.append(line[1:])
|
| 288 |
+
else:
|
| 289 |
+
add()
|
| 290 |
+
lines = []
|
| 291 |
+
|
| 292 |
+
add()
|
| 293 |
+
|
| 294 |
+
# Reverse the order, because newest items have to go first.
|
| 295 |
+
return reversed(strings)
|
| 296 |
+
|
| 297 |
+
def store_string(self, string: str) -> None:
|
| 298 |
+
# Save to file.
|
| 299 |
+
with open(self.filename, "ab") as f:
|
| 300 |
+
|
| 301 |
+
def write(t: str) -> None:
|
| 302 |
+
f.write(t.encode("utf-8"))
|
| 303 |
+
|
| 304 |
+
write(f"\n# {datetime.datetime.now()}\n")
|
| 305 |
+
for line in string.split("\n"):
|
| 306 |
+
write(f"+{line}\n")
|
lib/python3.12/site-packages/prompt_toolkit/key_binding/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .key_bindings import (
|
| 4 |
+
ConditionalKeyBindings,
|
| 5 |
+
DynamicKeyBindings,
|
| 6 |
+
KeyBindings,
|
| 7 |
+
KeyBindingsBase,
|
| 8 |
+
merge_key_bindings,
|
| 9 |
+
)
|
| 10 |
+
from .key_processor import KeyPress, KeyPressEvent
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
# key_bindings.
|
| 14 |
+
"ConditionalKeyBindings",
|
| 15 |
+
"DynamicKeyBindings",
|
| 16 |
+
"KeyBindings",
|
| 17 |
+
"KeyBindingsBase",
|
| 18 |
+
"merge_key_bindings",
|
| 19 |
+
# key_processor
|
| 20 |
+
"KeyPress",
|
| 21 |
+
"KeyPressEvent",
|
| 22 |
+
]
|