ZhengyangZhang commited on
Commit
6e50806
·
verified ·
1 Parent(s): a7ad2e3

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/prompt_toolkit/__init__.py +54 -0
  2. lib/python3.12/site-packages/prompt_toolkit/auto_suggest.py +177 -0
  3. lib/python3.12/site-packages/prompt_toolkit/buffer.py +2029 -0
  4. lib/python3.12/site-packages/prompt_toolkit/cache.py +127 -0
  5. lib/python3.12/site-packages/prompt_toolkit/contrib/__init__.py +0 -0
  6. lib/python3.12/site-packages/prompt_toolkit/contrib/__pycache__/__init__.cpython-312.pyc +0 -0
  7. lib/python3.12/site-packages/prompt_toolkit/contrib/completers/__init__.py +5 -0
  8. lib/python3.12/site-packages/prompt_toolkit/contrib/completers/__pycache__/__init__.cpython-312.pyc +0 -0
  9. lib/python3.12/site-packages/prompt_toolkit/contrib/completers/__pycache__/system.cpython-312.pyc +0 -0
  10. lib/python3.12/site-packages/prompt_toolkit/contrib/completers/system.py +64 -0
  11. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__init__.py +80 -0
  12. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/__init__.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/compiler.cpython-312.pyc +0 -0
  14. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/completion.cpython-312.pyc +0 -0
  15. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/lexer.cpython-312.pyc +0 -0
  16. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/regex_parser.cpython-312.pyc +0 -0
  17. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/__pycache__/validation.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/compiler.py +579 -0
  19. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/completion.py +100 -0
  20. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/lexer.py +94 -0
  21. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/regex_parser.py +279 -0
  22. lib/python3.12/site-packages/prompt_toolkit/contrib/regular_languages/validation.py +60 -0
  23. lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/__init__.py +8 -0
  24. lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/__pycache__/__init__.cpython-312.pyc +0 -0
  25. lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/__pycache__/server.cpython-312.pyc +0 -0
  26. lib/python3.12/site-packages/prompt_toolkit/contrib/ssh/server.py +178 -0
  27. lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__init__.py +7 -0
  28. lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/__init__.cpython-312.pyc +0 -0
  29. lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/log.cpython-312.pyc +0 -0
  30. lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/protocol.cpython-312.pyc +0 -0
  31. lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/__pycache__/server.cpython-312.pyc +0 -0
  32. lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/log.py +13 -0
  33. lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/protocol.py +209 -0
  34. lib/python3.12/site-packages/prompt_toolkit/contrib/telnet/server.py +428 -0
  35. lib/python3.12/site-packages/prompt_toolkit/cursor_shapes.py +117 -0
  36. lib/python3.12/site-packages/prompt_toolkit/data_structures.py +18 -0
  37. lib/python3.12/site-packages/prompt_toolkit/document.py +1182 -0
  38. lib/python3.12/site-packages/prompt_toolkit/enums.py +19 -0
  39. lib/python3.12/site-packages/prompt_toolkit/filters/__init__.py +71 -0
  40. lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/__init__.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/app.cpython-312.pyc +0 -0
  42. lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/base.cpython-312.pyc +0 -0
  43. lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/cli.cpython-312.pyc +0 -0
  44. lib/python3.12/site-packages/prompt_toolkit/filters/__pycache__/utils.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/prompt_toolkit/filters/app.py +419 -0
  46. lib/python3.12/site-packages/prompt_toolkit/filters/base.py +260 -0
  47. lib/python3.12/site-packages/prompt_toolkit/filters/cli.py +65 -0
  48. lib/python3.12/site-packages/prompt_toolkit/filters/utils.py +41 -0
  49. lib/python3.12/site-packages/prompt_toolkit/history.py +306 -0
  50. 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
+ ]