##// END OF EJS Templates
Fix typos
Andrew Kreimer -
Show More
@@ -1,401 +1,401
1 1 import re
2 2 import tokenize
3 3 from io import StringIO
4 4 from typing import Callable, List, Optional, Union, Generator, Tuple
5 5 import warnings
6 6
7 7 from prompt_toolkit.buffer import Buffer
8 8 from prompt_toolkit.key_binding import KeyPressEvent
9 9 from prompt_toolkit.key_binding.bindings import named_commands as nc
10 10 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
11 11 from prompt_toolkit.document import Document
12 12 from prompt_toolkit.history import History
13 13 from prompt_toolkit.shortcuts import PromptSession
14 14 from prompt_toolkit.layout.processors import (
15 15 Processor,
16 16 Transformation,
17 17 TransformationInput,
18 18 )
19 19
20 20 from IPython.core.getipython import get_ipython
21 21 from IPython.utils.tokenutil import generate_tokens
22 22
23 23 from .filters import pass_through
24 24
25 25
26 26 def _get_query(document: Document):
27 27 return document.lines[document.cursor_position_row]
28 28
29 29
30 30 class AppendAutoSuggestionInAnyLine(Processor):
31 31 """
32 32 Append the auto suggestion to lines other than the last (appending to the
33 33 last line is natively supported by the prompt toolkit).
34 34 """
35 35
36 36 def __init__(self, style: str = "class:auto-suggestion") -> None:
37 37 self.style = style
38 38
39 39 def apply_transformation(self, ti: TransformationInput) -> Transformation:
40 40 is_last_line = ti.lineno == ti.document.line_count - 1
41 41 is_active_line = ti.lineno == ti.document.cursor_position_row
42 42
43 43 if not is_last_line and is_active_line:
44 44 buffer = ti.buffer_control.buffer
45 45
46 46 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
47 47 suggestion = buffer.suggestion.text
48 48 else:
49 49 suggestion = ""
50 50
51 51 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
52 52 else:
53 53 return Transformation(fragments=ti.fragments)
54 54
55 55
56 56 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
57 57 """
58 58 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
59 59 suggestion from history. To do so it remembers the current position, but it
60 60 state need to carefully be cleared on the right events.
61 61 """
62 62
63 63 def __init__(
64 64 self,
65 65 ):
66 66 self.skip_lines = 0
67 67 self._connected_apps = []
68 68
69 69 def reset_history_position(self, _: Buffer):
70 70 self.skip_lines = 0
71 71
72 72 def disconnect(self):
73 73 for pt_app in self._connected_apps:
74 74 text_insert_event = pt_app.default_buffer.on_text_insert
75 75 text_insert_event.remove_handler(self.reset_history_position)
76 76
77 77 def connect(self, pt_app: PromptSession):
78 78 self._connected_apps.append(pt_app)
79 79 # note: `on_text_changed` could be used for a bit different behaviour
80 # on character deletion (i.e. reseting history position on backspace)
80 # on character deletion (i.e. resetting history position on backspace)
81 81 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
82 82 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
83 83
84 84 def get_suggestion(
85 85 self, buffer: Buffer, document: Document
86 86 ) -> Optional[Suggestion]:
87 87 text = _get_query(document)
88 88
89 89 if text.strip():
90 90 for suggestion, _ in self._find_next_match(
91 91 text, self.skip_lines, buffer.history
92 92 ):
93 93 return Suggestion(suggestion)
94 94
95 95 return None
96 96
97 97 def _dismiss(self, buffer, *args, **kwargs):
98 98 buffer.suggestion = None
99 99
100 100 def _find_match(
101 101 self, text: str, skip_lines: float, history: History, previous: bool
102 102 ) -> Generator[Tuple[str, float], None, None]:
103 103 """
104 104 text : str
105 105 Text content to find a match for, the user cursor is most of the
106 106 time at the end of this text.
107 107 skip_lines : float
108 108 number of items to skip in the search, this is used to indicate how
109 109 far in the list the user has navigated by pressing up or down.
110 110 The float type is used as the base value is +inf
111 111 history : History
112 112 prompt_toolkit History instance to fetch previous entries from.
113 113 previous : bool
114 114 Direction of the search, whether we are looking previous match
115 115 (True), or next match (False).
116 116
117 117 Yields
118 118 ------
119 119 Tuple with:
120 120 str:
121 121 current suggestion.
122 122 float:
123 123 will actually yield only ints, which is passed back via skip_lines,
124 124 which may be a +inf (float)
125 125
126 126
127 127 """
128 128 line_number = -1
129 129 for string in reversed(list(history.get_strings())):
130 130 for line in reversed(string.splitlines()):
131 131 line_number += 1
132 132 if not previous and line_number < skip_lines:
133 133 continue
134 134 # do not return empty suggestions as these
135 135 # close the auto-suggestion overlay (and are useless)
136 136 if line.startswith(text) and len(line) > len(text):
137 137 yield line[len(text) :], line_number
138 138 if previous and line_number >= skip_lines:
139 139 return
140 140
141 141 def _find_next_match(
142 142 self, text: str, skip_lines: float, history: History
143 143 ) -> Generator[Tuple[str, float], None, None]:
144 144 return self._find_match(text, skip_lines, history, previous=False)
145 145
146 146 def _find_previous_match(self, text: str, skip_lines: float, history: History):
147 147 return reversed(
148 148 list(self._find_match(text, skip_lines, history, previous=True))
149 149 )
150 150
151 151 def up(self, query: str, other_than: str, history: History) -> None:
152 152 for suggestion, line_number in self._find_next_match(
153 153 query, self.skip_lines, history
154 154 ):
155 155 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
156 156 # we want to switch from 'very.b' to 'very.a' because a) if the
157 157 # suggestion equals current text, prompt-toolkit aborts suggesting
158 158 # b) user likely would not be interested in 'very' anyways (they
159 159 # already typed it).
160 160 if query + suggestion != other_than:
161 161 self.skip_lines = line_number
162 162 break
163 163 else:
164 164 # no matches found, cycle back to beginning
165 165 self.skip_lines = 0
166 166
167 167 def down(self, query: str, other_than: str, history: History) -> None:
168 168 for suggestion, line_number in self._find_previous_match(
169 169 query, self.skip_lines, history
170 170 ):
171 171 if query + suggestion != other_than:
172 172 self.skip_lines = line_number
173 173 break
174 174 else:
175 175 # no matches found, cycle to end
176 176 for suggestion, line_number in self._find_previous_match(
177 177 query, float("Inf"), history
178 178 ):
179 179 if query + suggestion != other_than:
180 180 self.skip_lines = line_number
181 181 break
182 182
183 183
184 184 def accept_or_jump_to_end(event: KeyPressEvent):
185 185 """Apply autosuggestion or jump to end of line."""
186 186 buffer = event.current_buffer
187 187 d = buffer.document
188 188 after_cursor = d.text[d.cursor_position :]
189 189 lines = after_cursor.split("\n")
190 190 end_of_current_line = lines[0].strip()
191 191 suggestion = buffer.suggestion
192 192 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
193 193 buffer.insert_text(suggestion.text)
194 194 else:
195 195 nc.end_of_line(event)
196 196
197 197
198 198 def _deprected_accept_in_vi_insert_mode(event: KeyPressEvent):
199 199 """Accept autosuggestion or jump to end of line.
200 200
201 201 .. deprecated:: 8.12
202 202 Use `accept_or_jump_to_end` instead.
203 203 """
204 204 return accept_or_jump_to_end(event)
205 205
206 206
207 207 def accept(event: KeyPressEvent):
208 208 """Accept autosuggestion"""
209 209 buffer = event.current_buffer
210 210 suggestion = buffer.suggestion
211 211 if suggestion:
212 212 buffer.insert_text(suggestion.text)
213 213 else:
214 214 nc.forward_char(event)
215 215
216 216
217 217 def discard(event: KeyPressEvent):
218 218 """Discard autosuggestion"""
219 219 buffer = event.current_buffer
220 220 buffer.suggestion = None
221 221
222 222
223 223 def accept_word(event: KeyPressEvent):
224 224 """Fill partial autosuggestion by word"""
225 225 buffer = event.current_buffer
226 226 suggestion = buffer.suggestion
227 227 if suggestion:
228 228 t = re.split(r"(\S+\s+)", suggestion.text)
229 229 buffer.insert_text(next((x for x in t if x), ""))
230 230 else:
231 231 nc.forward_word(event)
232 232
233 233
234 234 def accept_character(event: KeyPressEvent):
235 235 """Fill partial autosuggestion by character"""
236 236 b = event.current_buffer
237 237 suggestion = b.suggestion
238 238 if suggestion and suggestion.text:
239 239 b.insert_text(suggestion.text[0])
240 240
241 241
242 242 def accept_and_keep_cursor(event: KeyPressEvent):
243 243 """Accept autosuggestion and keep cursor in place"""
244 244 buffer = event.current_buffer
245 245 old_position = buffer.cursor_position
246 246 suggestion = buffer.suggestion
247 247 if suggestion:
248 248 buffer.insert_text(suggestion.text)
249 249 buffer.cursor_position = old_position
250 250
251 251
252 252 def accept_and_move_cursor_left(event: KeyPressEvent):
253 253 """Accept autosuggestion and move cursor left in place"""
254 254 accept_and_keep_cursor(event)
255 255 nc.backward_char(event)
256 256
257 257
258 258 def _update_hint(buffer: Buffer):
259 259 if buffer.auto_suggest:
260 260 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
261 261 buffer.suggestion = suggestion
262 262
263 263
264 264 def backspace_and_resume_hint(event: KeyPressEvent):
265 265 """Resume autosuggestions after deleting last character"""
266 266 nc.backward_delete_char(event)
267 267 _update_hint(event.current_buffer)
268 268
269 269
270 270 def resume_hinting(event: KeyPressEvent):
271 271 """Resume autosuggestions"""
272 272 pass_through.reply(event)
273 273 # Order matters: if update happened first and event reply second, the
274 274 # suggestion would be auto-accepted if both actions are bound to same key.
275 275 _update_hint(event.current_buffer)
276 276
277 277
278 278 def up_and_update_hint(event: KeyPressEvent):
279 279 """Go up and update hint"""
280 280 current_buffer = event.current_buffer
281 281
282 282 current_buffer.auto_up(count=event.arg)
283 283 _update_hint(current_buffer)
284 284
285 285
286 286 def down_and_update_hint(event: KeyPressEvent):
287 287 """Go down and update hint"""
288 288 current_buffer = event.current_buffer
289 289
290 290 current_buffer.auto_down(count=event.arg)
291 291 _update_hint(current_buffer)
292 292
293 293
294 294 def accept_token(event: KeyPressEvent):
295 295 """Fill partial autosuggestion by token"""
296 296 b = event.current_buffer
297 297 suggestion = b.suggestion
298 298
299 299 if suggestion:
300 300 prefix = _get_query(b.document)
301 301 text = prefix + suggestion.text
302 302
303 303 tokens: List[Optional[str]] = [None, None, None]
304 304 substrings = [""]
305 305 i = 0
306 306
307 307 for token in generate_tokens(StringIO(text).readline):
308 308 if token.type == tokenize.NEWLINE:
309 309 index = len(text)
310 310 else:
311 311 index = text.index(token[1], len(substrings[-1]))
312 312 substrings.append(text[:index])
313 313 tokenized_so_far = substrings[-1]
314 314 if tokenized_so_far.startswith(prefix):
315 315 if i == 0 and len(tokenized_so_far) > len(prefix):
316 316 tokens[0] = tokenized_so_far[len(prefix) :]
317 317 substrings.append(tokenized_so_far)
318 318 i += 1
319 319 tokens[i] = token[1]
320 320 if i == 2:
321 321 break
322 322 i += 1
323 323
324 324 if tokens[0]:
325 325 to_insert: str
326 326 insert_text = substrings[-2]
327 327 if tokens[1] and len(tokens[1]) == 1:
328 328 insert_text = substrings[-1]
329 329 to_insert = insert_text[len(prefix) :]
330 330 b.insert_text(to_insert)
331 331 return
332 332
333 333 nc.forward_word(event)
334 334
335 335
336 336 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
337 337
338 338
339 339 def _swap_autosuggestion(
340 340 buffer: Buffer,
341 341 provider: NavigableAutoSuggestFromHistory,
342 342 direction_method: Callable,
343 343 ):
344 344 """
345 345 We skip most recent history entry (in either direction) if it equals the
346 346 current autosuggestion because if user cycles when auto-suggestion is shown
347 347 they most likely want something else than what was suggested (otherwise
348 348 they would have accepted the suggestion).
349 349 """
350 350 suggestion = buffer.suggestion
351 351 if not suggestion:
352 352 return
353 353
354 354 query = _get_query(buffer.document)
355 355 current = query + suggestion.text
356 356
357 357 direction_method(query=query, other_than=current, history=buffer.history)
358 358
359 359 new_suggestion = provider.get_suggestion(buffer, buffer.document)
360 360 buffer.suggestion = new_suggestion
361 361
362 362
363 363 def swap_autosuggestion_up(event: KeyPressEvent):
364 364 """Get next autosuggestion from history."""
365 365 shell = get_ipython()
366 366 provider = shell.auto_suggest
367 367
368 368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
369 369 return
370 370
371 371 return _swap_autosuggestion(
372 372 buffer=event.current_buffer, provider=provider, direction_method=provider.up
373 373 )
374 374
375 375
376 376 def swap_autosuggestion_down(event: KeyPressEvent):
377 377 """Get previous autosuggestion from history."""
378 378 shell = get_ipython()
379 379 provider = shell.auto_suggest
380 380
381 381 if not isinstance(provider, NavigableAutoSuggestFromHistory):
382 382 return
383 383
384 384 return _swap_autosuggestion(
385 385 buffer=event.current_buffer,
386 386 provider=provider,
387 387 direction_method=provider.down,
388 388 )
389 389
390 390
391 391 def __getattr__(key):
392 392 if key == "accept_in_vi_insert_mode":
393 393 warnings.warn(
394 394 "`accept_in_vi_insert_mode` is deprecated since IPython 8.12 and "
395 395 "renamed to `accept_or_jump_to_end`. Please update your configuration "
396 396 "accordingly",
397 397 DeprecationWarning,
398 398 stacklevel=2,
399 399 )
400 400 return _deprected_accept_in_vi_insert_mode
401 401 raise AttributeError
@@ -1,322 +1,322
1 1 """
2 2 Filters restricting scope of IPython Terminal shortcuts.
3 3 """
4 4
5 5 # Copyright (c) IPython Development Team.
6 6 # Distributed under the terms of the Modified BSD License.
7 7
8 8 import ast
9 9 import re
10 10 import signal
11 11 import sys
12 12 from typing import Callable, Dict, Union
13 13
14 14 from prompt_toolkit.application.current import get_app
15 15 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
16 16 from prompt_toolkit.key_binding import KeyPressEvent
17 17 from prompt_toolkit.filters import Condition, Filter, emacs_insert_mode, has_completions
18 18 from prompt_toolkit.filters import has_focus as has_focus_impl
19 19 from prompt_toolkit.filters import (
20 20 Always,
21 21 Never,
22 22 has_selection,
23 23 has_suggestion,
24 24 vi_insert_mode,
25 25 vi_mode,
26 26 )
27 27 from prompt_toolkit.layout.layout import FocusableElement
28 28
29 29 from IPython.core.getipython import get_ipython
30 30 from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
31 31 from IPython.terminal.shortcuts import auto_suggest
32 32 from IPython.utils.decorators import undoc
33 33
34 34
35 35 @undoc
36 36 @Condition
37 37 def cursor_in_leading_ws():
38 38 before = get_app().current_buffer.document.current_line_before_cursor
39 39 return (not before) or before.isspace()
40 40
41 41
42 42 def has_focus(value: FocusableElement):
43 43 """Wrapper around has_focus adding a nice `__name__` to tester function"""
44 44 tester = has_focus_impl(value).func
45 45 tester.__name__ = f"is_focused({value})"
46 46 return Condition(tester)
47 47
48 48
49 49 @undoc
50 50 @Condition
51 51 def has_line_below() -> bool:
52 52 document = get_app().current_buffer.document
53 53 return document.cursor_position_row < len(document.lines) - 1
54 54
55 55
56 56 @undoc
57 57 @Condition
58 58 def is_cursor_at_the_end_of_line() -> bool:
59 59 document = get_app().current_buffer.document
60 60 return document.is_cursor_at_the_end_of_line
61 61
62 62
63 63 @undoc
64 64 @Condition
65 65 def has_line_above() -> bool:
66 66 document = get_app().current_buffer.document
67 67 return document.cursor_position_row != 0
68 68
69 69
70 70 @Condition
71 71 def ebivim():
72 72 shell = get_ipython()
73 73 return shell.emacs_bindings_in_vi_insert_mode
74 74
75 75
76 76 @Condition
77 77 def supports_suspend():
78 78 return hasattr(signal, "SIGTSTP")
79 79
80 80
81 81 @Condition
82 82 def auto_match():
83 83 shell = get_ipython()
84 84 return shell.auto_match
85 85
86 86
87 87 def all_quotes_paired(quote, buf):
88 88 paired = True
89 89 i = 0
90 90 while i < len(buf):
91 91 c = buf[i]
92 92 if c == quote:
93 93 paired = not paired
94 94 elif c == "\\":
95 95 i += 1
96 96 i += 1
97 97 return paired
98 98
99 99
100 100 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
101 101 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
102 102
103 103
104 104 def preceding_text(pattern: Union[str, Callable]):
105 105 if pattern in _preceding_text_cache:
106 106 return _preceding_text_cache[pattern]
107 107
108 108 if callable(pattern):
109 109
110 110 def _preceding_text():
111 111 app = get_app()
112 112 before_cursor = app.current_buffer.document.current_line_before_cursor
113 113 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
114 114 return bool(pattern(before_cursor)) # type: ignore[operator]
115 115
116 116 else:
117 117 m = re.compile(pattern)
118 118
119 119 def _preceding_text():
120 120 app = get_app()
121 121 before_cursor = app.current_buffer.document.current_line_before_cursor
122 122 return bool(m.match(before_cursor))
123 123
124 124 _preceding_text.__name__ = f"preceding_text({pattern!r})"
125 125
126 126 condition = Condition(_preceding_text)
127 127 _preceding_text_cache[pattern] = condition
128 128 return condition
129 129
130 130
131 131 def following_text(pattern):
132 132 try:
133 133 return _following_text_cache[pattern]
134 134 except KeyError:
135 135 pass
136 136 m = re.compile(pattern)
137 137
138 138 def _following_text():
139 139 app = get_app()
140 140 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
141 141
142 142 _following_text.__name__ = f"following_text({pattern!r})"
143 143
144 144 condition = Condition(_following_text)
145 145 _following_text_cache[pattern] = condition
146 146 return condition
147 147
148 148
149 149 @Condition
150 150 def not_inside_unclosed_string():
151 151 app = get_app()
152 152 s = app.current_buffer.document.text_before_cursor
153 153 # remove escaped quotes
154 154 s = s.replace('\\"', "").replace("\\'", "")
155 155 # remove triple-quoted string literals
156 156 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
157 157 # remove single-quoted string literals
158 158 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
159 159 return not ('"' in s or "'" in s)
160 160
161 161
162 162 @Condition
163 163 def navigable_suggestions():
164 164 shell = get_ipython()
165 165 return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
166 166
167 167
168 168 @Condition
169 169 def readline_like_completions():
170 170 shell = get_ipython()
171 171 return shell.display_completions == "readlinelike"
172 172
173 173
174 174 @Condition
175 175 def is_windows_os():
176 176 return sys.platform == "win32"
177 177
178 178
179 179 class PassThrough(Filter):
180 180 """A filter allowing to implement pass-through behaviour of keybindings.
181 181
182 182 Prompt toolkit key processor dispatches only one event per binding match,
183 183 which means that adding a new shortcut will suppress the old shortcut
184 184 if the keybindings are the same (unless one is filtered out).
185 185
186 186 To stop a shortcut binding from suppressing other shortcuts:
187 187 - add the `pass_through` filter to list of filter, and
188 188 - call `pass_through.reply(event)` in the shortcut handler.
189 189 """
190 190
191 191 def __init__(self):
192 192 self._is_replying = False
193 193
194 194 def reply(self, event: KeyPressEvent):
195 195 self._is_replying = True
196 196 try:
197 197 event.key_processor.reset()
198 198 event.key_processor.feed_multiple(event.key_sequence)
199 199 event.key_processor.process_keys()
200 200 finally:
201 201 self._is_replying = False
202 202
203 203 def __call__(self):
204 204 return not self._is_replying
205 205
206 206
207 207 pass_through = PassThrough()
208 208
209 209 # these one is callable and re-used multiple times hence needs to be
210 # only defined once beforhand so that transforming back to human-readable
210 # only defined once beforehand so that transforming back to human-readable
211 211 # names works well in the documentation.
212 212 default_buffer_focused = has_focus(DEFAULT_BUFFER)
213 213
214 214 KEYBINDING_FILTERS = {
215 215 "always": Always(),
216 216 # never is used for exposing commands which have no default keybindings
217 217 "never": Never(),
218 218 "has_line_below": has_line_below,
219 219 "has_line_above": has_line_above,
220 220 "is_cursor_at_the_end_of_line": is_cursor_at_the_end_of_line,
221 221 "has_selection": has_selection,
222 222 "has_suggestion": has_suggestion,
223 223 "vi_mode": vi_mode,
224 224 "vi_insert_mode": vi_insert_mode,
225 225 "emacs_insert_mode": emacs_insert_mode,
226 226 # https://github.com/ipython/ipython/pull/12603 argued for inclusion of
227 227 # emacs key bindings with a configurable `emacs_bindings_in_vi_insert_mode`
228 228 # toggle; when the toggle is on user can access keybindigns like `ctrl + e`
229 229 # in vi insert mode. Because some of the emacs bindings involve `escape`
230 230 # followed by another key, e.g. `escape` followed by `f`, prompt-toolkit
231 231 # needs to wait to see if there will be another character typed in before
232 232 # executing pure `escape` keybinding; in vi insert mode `escape` switches to
233 233 # command mode which is common and performance critical action for vi users.
234 234 # To avoid the delay users employ a workaround:
235 235 # https://github.com/ipython/ipython/issues/13443#issuecomment-1032753703
236 236 # which involves switching `emacs_bindings_in_vi_insert_mode` off.
237 237 #
238 238 # For the workaround to work:
239 239 # 1) end users need to toggle `emacs_bindings_in_vi_insert_mode` off
240 240 # 2) all keybindings which would involve `escape` need to respect that
241 241 # toggle by including either:
242 242 # - `vi_insert_mode & ebivim` for actions which have emacs keybindings
243 243 # predefined upstream in prompt-toolkit, or
244 244 # - `emacs_like_insert_mode` for actions which do not have existing
245 245 # emacs keybindings predefined upstream (or need overriding of the
246 246 # upstream bindings to modify behaviour), defined below.
247 247 "emacs_like_insert_mode": (vi_insert_mode & ebivim) | emacs_insert_mode,
248 248 "has_completions": has_completions,
249 249 "insert_mode": vi_insert_mode | emacs_insert_mode,
250 250 "default_buffer_focused": default_buffer_focused,
251 251 "search_buffer_focused": has_focus(SEARCH_BUFFER),
252 252 # `ebivim` stands for emacs bindings in vi insert mode
253 253 "ebivim": ebivim,
254 254 "supports_suspend": supports_suspend,
255 255 "is_windows_os": is_windows_os,
256 256 "auto_match": auto_match,
257 257 "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
258 258 "not_inside_unclosed_string": not_inside_unclosed_string,
259 259 "readline_like_completions": readline_like_completions,
260 260 "preceded_by_paired_double_quotes": preceding_text(
261 261 lambda line: all_quotes_paired('"', line)
262 262 ),
263 263 "preceded_by_paired_single_quotes": preceding_text(
264 264 lambda line: all_quotes_paired("'", line)
265 265 ),
266 266 "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
267 267 "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
268 268 "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
269 269 "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
270 270 "preceded_by_opening_round_paren": preceding_text(r".*\($"),
271 271 "preceded_by_opening_bracket": preceding_text(r".*\[$"),
272 272 "preceded_by_opening_brace": preceding_text(r".*\{$"),
273 273 "preceded_by_double_quote": preceding_text('.*"$'),
274 274 "preceded_by_single_quote": preceding_text(r".*'$"),
275 275 "followed_by_closing_round_paren": following_text(r"^\)"),
276 276 "followed_by_closing_bracket": following_text(r"^\]"),
277 277 "followed_by_closing_brace": following_text(r"^\}"),
278 278 "followed_by_double_quote": following_text('^"'),
279 279 "followed_by_single_quote": following_text("^'"),
280 280 "navigable_suggestions": navigable_suggestions,
281 281 "cursor_in_leading_ws": cursor_in_leading_ws,
282 282 "pass_through": pass_through,
283 283 }
284 284
285 285
286 286 def eval_node(node: Union[ast.AST, None]):
287 287 if node is None:
288 288 return None
289 289 if isinstance(node, ast.Expression):
290 290 return eval_node(node.body)
291 291 if isinstance(node, ast.BinOp):
292 292 left = eval_node(node.left)
293 293 right = eval_node(node.right)
294 294 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
295 295 if dunders:
296 296 return getattr(left, dunders[0])(right)
297 297 raise ValueError(f"Unknown binary operation: {node.op}")
298 298 if isinstance(node, ast.UnaryOp):
299 299 value = eval_node(node.operand)
300 300 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
301 301 if dunders:
302 302 return getattr(value, dunders[0])()
303 303 raise ValueError(f"Unknown unary operation: {node.op}")
304 304 if isinstance(node, ast.Name):
305 305 if node.id in KEYBINDING_FILTERS:
306 306 return KEYBINDING_FILTERS[node.id]
307 307 else:
308 308 sep = "\n - "
309 309 known_filters = sep.join(sorted(KEYBINDING_FILTERS))
310 310 raise NameError(
311 311 f"{node.id} is not a known shortcut filter."
312 312 f" Known filters are: {sep}{known_filters}."
313 313 )
314 314 raise ValueError("Unhandled node", ast.dump(node))
315 315
316 316
317 317 def filter_from_string(code: str):
318 318 expression = ast.parse(code, mode="eval")
319 319 return eval_node(expression)
320 320
321 321
322 322 __all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
General Comments 0
You need to be logged in to leave comments. Login now