##// END OF EJS Templates
Only enable "resume hinting" with right arrow if at the EOL (#14052)...
Matthias Bussonnier -
r28281:78fcf822 merge
parent child Browse files
Show More
@@ -1,627 +1,629 b''
1 """
1 """
2 Module to define and register Terminal IPython shortcuts with
2 Module to define and register Terminal IPython shortcuts with
3 :mod:`prompt_toolkit`
3 :mod:`prompt_toolkit`
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 import os
9 import os
10 import signal
10 import signal
11 import sys
11 import sys
12 import warnings
12 import warnings
13 from dataclasses import dataclass
13 from dataclasses import dataclass
14 from typing import Callable, Any, Optional, List
14 from typing import Callable, Any, Optional, List
15
15
16 from prompt_toolkit.application.current import get_app
16 from prompt_toolkit.application.current import get_app
17 from prompt_toolkit.key_binding import KeyBindings
17 from prompt_toolkit.key_binding import KeyBindings
18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
19 from prompt_toolkit.key_binding.bindings import named_commands as nc
19 from prompt_toolkit.key_binding.bindings import named_commands as nc
20 from prompt_toolkit.key_binding.bindings.completion import (
20 from prompt_toolkit.key_binding.bindings.completion import (
21 display_completions_like_readline,
21 display_completions_like_readline,
22 )
22 )
23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
24 from prompt_toolkit.filters import Condition
24 from prompt_toolkit.filters import Condition
25
25
26 from IPython.core.getipython import get_ipython
26 from IPython.core.getipython import get_ipython
27 from IPython.terminal.shortcuts import auto_match as match
27 from IPython.terminal.shortcuts import auto_match as match
28 from IPython.terminal.shortcuts import auto_suggest
28 from IPython.terminal.shortcuts import auto_suggest
29 from IPython.terminal.shortcuts.filters import filter_from_string
29 from IPython.terminal.shortcuts.filters import filter_from_string
30 from IPython.utils.decorators import undoc
30 from IPython.utils.decorators import undoc
31
31
32 from prompt_toolkit.enums import DEFAULT_BUFFER
32 from prompt_toolkit.enums import DEFAULT_BUFFER
33
33
34 __all__ = ["create_ipython_shortcuts"]
34 __all__ = ["create_ipython_shortcuts"]
35
35
36
36
37 @dataclass
37 @dataclass
38 class BaseBinding:
38 class BaseBinding:
39 command: Callable[[KeyPressEvent], Any]
39 command: Callable[[KeyPressEvent], Any]
40 keys: List[str]
40 keys: List[str]
41
41
42
42
43 @dataclass
43 @dataclass
44 class RuntimeBinding(BaseBinding):
44 class RuntimeBinding(BaseBinding):
45 filter: Condition
45 filter: Condition
46
46
47
47
48 @dataclass
48 @dataclass
49 class Binding(BaseBinding):
49 class Binding(BaseBinding):
50 # while filter could be created by referencing variables directly (rather
50 # while filter could be created by referencing variables directly (rather
51 # than created from strings), by using strings we ensure that users will
51 # than created from strings), by using strings we ensure that users will
52 # be able to create filters in configuration (e.g. JSON) files too, which
52 # be able to create filters in configuration (e.g. JSON) files too, which
53 # also benefits the documentation by enforcing human-readable filter names.
53 # also benefits the documentation by enforcing human-readable filter names.
54 condition: Optional[str] = None
54 condition: Optional[str] = None
55
55
56 def __post_init__(self):
56 def __post_init__(self):
57 if self.condition:
57 if self.condition:
58 self.filter = filter_from_string(self.condition)
58 self.filter = filter_from_string(self.condition)
59 else:
59 else:
60 self.filter = None
60 self.filter = None
61
61
62
62
63 def create_identifier(handler: Callable):
63 def create_identifier(handler: Callable):
64 parts = handler.__module__.split(".")
64 parts = handler.__module__.split(".")
65 name = handler.__name__
65 name = handler.__name__
66 package = parts[0]
66 package = parts[0]
67 if len(parts) > 1:
67 if len(parts) > 1:
68 final_module = parts[-1]
68 final_module = parts[-1]
69 return f"{package}:{final_module}.{name}"
69 return f"{package}:{final_module}.{name}"
70 else:
70 else:
71 return f"{package}:{name}"
71 return f"{package}:{name}"
72
72
73
73
74 AUTO_MATCH_BINDINGS = [
74 AUTO_MATCH_BINDINGS = [
75 *[
75 *[
76 Binding(
76 Binding(
77 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
77 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
78 )
78 )
79 for key, cmd in match.auto_match_parens.items()
79 for key, cmd in match.auto_match_parens.items()
80 ],
80 ],
81 *[
81 *[
82 # raw string
82 # raw string
83 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
83 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
84 for key, cmd in match.auto_match_parens_raw_string.items()
84 for key, cmd in match.auto_match_parens_raw_string.items()
85 ],
85 ],
86 Binding(
86 Binding(
87 match.double_quote,
87 match.double_quote,
88 ['"'],
88 ['"'],
89 "focused_insert"
89 "focused_insert"
90 " & auto_match"
90 " & auto_match"
91 " & not_inside_unclosed_string"
91 " & not_inside_unclosed_string"
92 " & preceded_by_paired_double_quotes"
92 " & preceded_by_paired_double_quotes"
93 " & followed_by_closing_paren_or_end",
93 " & followed_by_closing_paren_or_end",
94 ),
94 ),
95 Binding(
95 Binding(
96 match.single_quote,
96 match.single_quote,
97 ["'"],
97 ["'"],
98 "focused_insert"
98 "focused_insert"
99 " & auto_match"
99 " & auto_match"
100 " & not_inside_unclosed_string"
100 " & not_inside_unclosed_string"
101 " & preceded_by_paired_single_quotes"
101 " & preceded_by_paired_single_quotes"
102 " & followed_by_closing_paren_or_end",
102 " & followed_by_closing_paren_or_end",
103 ),
103 ),
104 Binding(
104 Binding(
105 match.docstring_double_quotes,
105 match.docstring_double_quotes,
106 ['"'],
106 ['"'],
107 "focused_insert"
107 "focused_insert"
108 " & auto_match"
108 " & auto_match"
109 " & not_inside_unclosed_string"
109 " & not_inside_unclosed_string"
110 " & preceded_by_two_double_quotes",
110 " & preceded_by_two_double_quotes",
111 ),
111 ),
112 Binding(
112 Binding(
113 match.docstring_single_quotes,
113 match.docstring_single_quotes,
114 ["'"],
114 ["'"],
115 "focused_insert"
115 "focused_insert"
116 " & auto_match"
116 " & auto_match"
117 " & not_inside_unclosed_string"
117 " & not_inside_unclosed_string"
118 " & preceded_by_two_single_quotes",
118 " & preceded_by_two_single_quotes",
119 ),
119 ),
120 Binding(
120 Binding(
121 match.skip_over,
121 match.skip_over,
122 [")"],
122 [")"],
123 "focused_insert & auto_match & followed_by_closing_round_paren",
123 "focused_insert & auto_match & followed_by_closing_round_paren",
124 ),
124 ),
125 Binding(
125 Binding(
126 match.skip_over,
126 match.skip_over,
127 ["]"],
127 ["]"],
128 "focused_insert & auto_match & followed_by_closing_bracket",
128 "focused_insert & auto_match & followed_by_closing_bracket",
129 ),
129 ),
130 Binding(
130 Binding(
131 match.skip_over,
131 match.skip_over,
132 ["}"],
132 ["}"],
133 "focused_insert & auto_match & followed_by_closing_brace",
133 "focused_insert & auto_match & followed_by_closing_brace",
134 ),
134 ),
135 Binding(
135 Binding(
136 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
136 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
137 ),
137 ),
138 Binding(
138 Binding(
139 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
139 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
140 ),
140 ),
141 Binding(
141 Binding(
142 match.delete_pair,
142 match.delete_pair,
143 ["backspace"],
143 ["backspace"],
144 "focused_insert"
144 "focused_insert"
145 " & preceded_by_opening_round_paren"
145 " & preceded_by_opening_round_paren"
146 " & auto_match"
146 " & auto_match"
147 " & followed_by_closing_round_paren",
147 " & followed_by_closing_round_paren",
148 ),
148 ),
149 Binding(
149 Binding(
150 match.delete_pair,
150 match.delete_pair,
151 ["backspace"],
151 ["backspace"],
152 "focused_insert"
152 "focused_insert"
153 " & preceded_by_opening_bracket"
153 " & preceded_by_opening_bracket"
154 " & auto_match"
154 " & auto_match"
155 " & followed_by_closing_bracket",
155 " & followed_by_closing_bracket",
156 ),
156 ),
157 Binding(
157 Binding(
158 match.delete_pair,
158 match.delete_pair,
159 ["backspace"],
159 ["backspace"],
160 "focused_insert"
160 "focused_insert"
161 " & preceded_by_opening_brace"
161 " & preceded_by_opening_brace"
162 " & auto_match"
162 " & auto_match"
163 " & followed_by_closing_brace",
163 " & followed_by_closing_brace",
164 ),
164 ),
165 Binding(
165 Binding(
166 match.delete_pair,
166 match.delete_pair,
167 ["backspace"],
167 ["backspace"],
168 "focused_insert"
168 "focused_insert"
169 " & preceded_by_double_quote"
169 " & preceded_by_double_quote"
170 " & auto_match"
170 " & auto_match"
171 " & followed_by_double_quote",
171 " & followed_by_double_quote",
172 ),
172 ),
173 Binding(
173 Binding(
174 match.delete_pair,
174 match.delete_pair,
175 ["backspace"],
175 ["backspace"],
176 "focused_insert"
176 "focused_insert"
177 " & preceded_by_single_quote"
177 " & preceded_by_single_quote"
178 " & auto_match"
178 " & auto_match"
179 " & followed_by_single_quote",
179 " & followed_by_single_quote",
180 ),
180 ),
181 ]
181 ]
182
182
183 AUTO_SUGGEST_BINDINGS = [
183 AUTO_SUGGEST_BINDINGS = [
184 # there are two reasons for re-defining bindings defined upstream:
184 # there are two reasons for re-defining bindings defined upstream:
185 # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
185 # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
186 # 2) prompt-toolkit checks if we are at the end of text, not end of line
186 # 2) prompt-toolkit checks if we are at the end of text, not end of line
187 # hence it does not work in multi-line mode of navigable provider
187 # hence it does not work in multi-line mode of navigable provider
188 Binding(
188 Binding(
189 auto_suggest.accept_or_jump_to_end,
189 auto_suggest.accept_or_jump_to_end,
190 ["end"],
190 ["end"],
191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
192 ),
192 ),
193 Binding(
193 Binding(
194 auto_suggest.accept_or_jump_to_end,
194 auto_suggest.accept_or_jump_to_end,
195 ["c-e"],
195 ["c-e"],
196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
197 ),
197 ),
198 Binding(
198 Binding(
199 auto_suggest.accept,
199 auto_suggest.accept,
200 ["c-f"],
200 ["c-f"],
201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
202 ),
202 ),
203 Binding(
203 Binding(
204 auto_suggest.accept,
204 auto_suggest.accept,
205 ["right"],
205 ["right"],
206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
207 ),
207 ),
208 Binding(
208 Binding(
209 auto_suggest.accept_word,
209 auto_suggest.accept_word,
210 ["escape", "f"],
210 ["escape", "f"],
211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
212 ),
212 ),
213 Binding(
213 Binding(
214 auto_suggest.accept_token,
214 auto_suggest.accept_token,
215 ["c-right"],
215 ["c-right"],
216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
217 ),
217 ),
218 Binding(
218 Binding(
219 auto_suggest.discard,
219 auto_suggest.discard,
220 ["escape"],
220 ["escape"],
221 # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
221 # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
222 # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
222 # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
224 ),
224 ),
225 Binding(
225 Binding(
226 auto_suggest.discard,
226 auto_suggest.discard,
227 ["delete"],
227 ["delete"],
228 "has_suggestion & default_buffer_focused & emacs_insert_mode",
228 "has_suggestion & default_buffer_focused & emacs_insert_mode",
229 ),
229 ),
230 Binding(
230 Binding(
231 auto_suggest.swap_autosuggestion_up,
231 auto_suggest.swap_autosuggestion_up,
232 ["c-up"],
232 ["c-up"],
233 "navigable_suggestions"
233 "navigable_suggestions"
234 " & ~has_line_above"
234 " & ~has_line_above"
235 " & has_suggestion"
235 " & has_suggestion"
236 " & default_buffer_focused",
236 " & default_buffer_focused",
237 ),
237 ),
238 Binding(
238 Binding(
239 auto_suggest.swap_autosuggestion_down,
239 auto_suggest.swap_autosuggestion_down,
240 ["c-down"],
240 ["c-down"],
241 "navigable_suggestions"
241 "navigable_suggestions"
242 " & ~has_line_below"
242 " & ~has_line_below"
243 " & has_suggestion"
243 " & has_suggestion"
244 " & default_buffer_focused",
244 " & default_buffer_focused",
245 ),
245 ),
246 Binding(
246 Binding(
247 auto_suggest.up_and_update_hint,
247 auto_suggest.up_and_update_hint,
248 ["c-up"],
248 ["c-up"],
249 "has_line_above & navigable_suggestions & default_buffer_focused",
249 "has_line_above & navigable_suggestions & default_buffer_focused",
250 ),
250 ),
251 Binding(
251 Binding(
252 auto_suggest.down_and_update_hint,
252 auto_suggest.down_and_update_hint,
253 ["c-down"],
253 ["c-down"],
254 "has_line_below & navigable_suggestions & default_buffer_focused",
254 "has_line_below & navigable_suggestions & default_buffer_focused",
255 ),
255 ),
256 Binding(
256 Binding(
257 auto_suggest.accept_character,
257 auto_suggest.accept_character,
258 ["escape", "right"],
258 ["escape", "right"],
259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
260 ),
260 ),
261 Binding(
261 Binding(
262 auto_suggest.accept_and_move_cursor_left,
262 auto_suggest.accept_and_move_cursor_left,
263 ["c-left"],
263 ["c-left"],
264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
265 ),
265 ),
266 Binding(
266 Binding(
267 auto_suggest.accept_and_keep_cursor,
267 auto_suggest.accept_and_keep_cursor,
268 ["escape", "down"],
268 ["escape", "down"],
269 "has_suggestion & default_buffer_focused & emacs_insert_mode",
269 "has_suggestion & default_buffer_focused & emacs_insert_mode",
270 ),
270 ),
271 Binding(
271 Binding(
272 auto_suggest.backspace_and_resume_hint,
272 auto_suggest.backspace_and_resume_hint,
273 ["backspace"],
273 ["backspace"],
274 # no `has_suggestion` here to allow resuming if no suggestion
274 # no `has_suggestion` here to allow resuming if no suggestion
275 "default_buffer_focused & emacs_like_insert_mode",
275 "default_buffer_focused & emacs_like_insert_mode",
276 ),
276 ),
277 Binding(
277 Binding(
278 auto_suggest.resume_hinting,
278 auto_suggest.resume_hinting,
279 ["right"],
279 ["right"],
280 "default_buffer_focused & emacs_like_insert_mode",
280 "is_cursor_at_the_end_of_line"
281 " & default_buffer_focused"
282 " & emacs_like_insert_mode",
281 ),
283 ),
282 ]
284 ]
283
285
284
286
285 SIMPLE_CONTROL_BINDINGS = [
287 SIMPLE_CONTROL_BINDINGS = [
286 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
288 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
287 for key, cmd in {
289 for key, cmd in {
288 "c-a": nc.beginning_of_line,
290 "c-a": nc.beginning_of_line,
289 "c-b": nc.backward_char,
291 "c-b": nc.backward_char,
290 "c-k": nc.kill_line,
292 "c-k": nc.kill_line,
291 "c-w": nc.backward_kill_word,
293 "c-w": nc.backward_kill_word,
292 "c-y": nc.yank,
294 "c-y": nc.yank,
293 "c-_": nc.undo,
295 "c-_": nc.undo,
294 }.items()
296 }.items()
295 ]
297 ]
296
298
297
299
298 ALT_AND_COMOBO_CONTROL_BINDINGS = [
300 ALT_AND_COMOBO_CONTROL_BINDINGS = [
299 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
301 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
300 for keys, cmd in {
302 for keys, cmd in {
301 # Control Combos
303 # Control Combos
302 ("c-x", "c-e"): nc.edit_and_execute,
304 ("c-x", "c-e"): nc.edit_and_execute,
303 ("c-x", "e"): nc.edit_and_execute,
305 ("c-x", "e"): nc.edit_and_execute,
304 # Alt
306 # Alt
305 ("escape", "b"): nc.backward_word,
307 ("escape", "b"): nc.backward_word,
306 ("escape", "c"): nc.capitalize_word,
308 ("escape", "c"): nc.capitalize_word,
307 ("escape", "d"): nc.kill_word,
309 ("escape", "d"): nc.kill_word,
308 ("escape", "h"): nc.backward_kill_word,
310 ("escape", "h"): nc.backward_kill_word,
309 ("escape", "l"): nc.downcase_word,
311 ("escape", "l"): nc.downcase_word,
310 ("escape", "u"): nc.uppercase_word,
312 ("escape", "u"): nc.uppercase_word,
311 ("escape", "y"): nc.yank_pop,
313 ("escape", "y"): nc.yank_pop,
312 ("escape", "."): nc.yank_last_arg,
314 ("escape", "."): nc.yank_last_arg,
313 }.items()
315 }.items()
314 ]
316 ]
315
317
316
318
317 def add_binding(bindings: KeyBindings, binding: Binding):
319 def add_binding(bindings: KeyBindings, binding: Binding):
318 bindings.add(
320 bindings.add(
319 *binding.keys,
321 *binding.keys,
320 **({"filter": binding.filter} if binding.filter is not None else {}),
322 **({"filter": binding.filter} if binding.filter is not None else {}),
321 )(binding.command)
323 )(binding.command)
322
324
323
325
324 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
326 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
325 """Set up the prompt_toolkit keyboard shortcuts for IPython.
327 """Set up the prompt_toolkit keyboard shortcuts for IPython.
326
328
327 Parameters
329 Parameters
328 ----------
330 ----------
329 shell: InteractiveShell
331 shell: InteractiveShell
330 The current IPython shell Instance
332 The current IPython shell Instance
331 skip: List[Binding]
333 skip: List[Binding]
332 Bindings to skip.
334 Bindings to skip.
333
335
334 Returns
336 Returns
335 -------
337 -------
336 KeyBindings
338 KeyBindings
337 the keybinding instance for prompt toolkit.
339 the keybinding instance for prompt toolkit.
338
340
339 """
341 """
340 kb = KeyBindings()
342 kb = KeyBindings()
341 skip = skip or []
343 skip = skip or []
342 for binding in KEY_BINDINGS:
344 for binding in KEY_BINDINGS:
343 skip_this_one = False
345 skip_this_one = False
344 for to_skip in skip:
346 for to_skip in skip:
345 if (
347 if (
346 to_skip.command == binding.command
348 to_skip.command == binding.command
347 and to_skip.filter == binding.filter
349 and to_skip.filter == binding.filter
348 and to_skip.keys == binding.keys
350 and to_skip.keys == binding.keys
349 ):
351 ):
350 skip_this_one = True
352 skip_this_one = True
351 break
353 break
352 if skip_this_one:
354 if skip_this_one:
353 continue
355 continue
354 add_binding(kb, binding)
356 add_binding(kb, binding)
355
357
356 def get_input_mode(self):
358 def get_input_mode(self):
357 app = get_app()
359 app = get_app()
358 app.ttimeoutlen = shell.ttimeoutlen
360 app.ttimeoutlen = shell.ttimeoutlen
359 app.timeoutlen = shell.timeoutlen
361 app.timeoutlen = shell.timeoutlen
360
362
361 return self._input_mode
363 return self._input_mode
362
364
363 def set_input_mode(self, mode):
365 def set_input_mode(self, mode):
364 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
366 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
365 cursor = "\x1b[{} q".format(shape)
367 cursor = "\x1b[{} q".format(shape)
366
368
367 sys.stdout.write(cursor)
369 sys.stdout.write(cursor)
368 sys.stdout.flush()
370 sys.stdout.flush()
369
371
370 self._input_mode = mode
372 self._input_mode = mode
371
373
372 if shell.editing_mode == "vi" and shell.modal_cursor:
374 if shell.editing_mode == "vi" and shell.modal_cursor:
373 ViState._input_mode = InputMode.INSERT # type: ignore
375 ViState._input_mode = InputMode.INSERT # type: ignore
374 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
376 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
375
377
376 return kb
378 return kb
377
379
378
380
379 def reformat_and_execute(event):
381 def reformat_and_execute(event):
380 """Reformat code and execute it"""
382 """Reformat code and execute it"""
381 shell = get_ipython()
383 shell = get_ipython()
382 reformat_text_before_cursor(
384 reformat_text_before_cursor(
383 event.current_buffer, event.current_buffer.document, shell
385 event.current_buffer, event.current_buffer.document, shell
384 )
386 )
385 event.current_buffer.validate_and_handle()
387 event.current_buffer.validate_and_handle()
386
388
387
389
388 def reformat_text_before_cursor(buffer, document, shell):
390 def reformat_text_before_cursor(buffer, document, shell):
389 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
391 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
390 try:
392 try:
391 formatted_text = shell.reformat_handler(text)
393 formatted_text = shell.reformat_handler(text)
392 buffer.insert_text(formatted_text)
394 buffer.insert_text(formatted_text)
393 except Exception as e:
395 except Exception as e:
394 buffer.insert_text(text)
396 buffer.insert_text(text)
395
397
396
398
397 def handle_return_or_newline_or_execute(event):
399 def handle_return_or_newline_or_execute(event):
398 shell = get_ipython()
400 shell = get_ipython()
399 if getattr(shell, "handle_return", None):
401 if getattr(shell, "handle_return", None):
400 return shell.handle_return(shell)(event)
402 return shell.handle_return(shell)(event)
401 else:
403 else:
402 return newline_or_execute_outer(shell)(event)
404 return newline_or_execute_outer(shell)(event)
403
405
404
406
405 def newline_or_execute_outer(shell):
407 def newline_or_execute_outer(shell):
406 def newline_or_execute(event):
408 def newline_or_execute(event):
407 """When the user presses return, insert a newline or execute the code."""
409 """When the user presses return, insert a newline or execute the code."""
408 b = event.current_buffer
410 b = event.current_buffer
409 d = b.document
411 d = b.document
410
412
411 if b.complete_state:
413 if b.complete_state:
412 cc = b.complete_state.current_completion
414 cc = b.complete_state.current_completion
413 if cc:
415 if cc:
414 b.apply_completion(cc)
416 b.apply_completion(cc)
415 else:
417 else:
416 b.cancel_completion()
418 b.cancel_completion()
417 return
419 return
418
420
419 # If there's only one line, treat it as if the cursor is at the end.
421 # If there's only one line, treat it as if the cursor is at the end.
420 # See https://github.com/ipython/ipython/issues/10425
422 # See https://github.com/ipython/ipython/issues/10425
421 if d.line_count == 1:
423 if d.line_count == 1:
422 check_text = d.text
424 check_text = d.text
423 else:
425 else:
424 check_text = d.text[: d.cursor_position]
426 check_text = d.text[: d.cursor_position]
425 status, indent = shell.check_complete(check_text)
427 status, indent = shell.check_complete(check_text)
426
428
427 # if all we have after the cursor is whitespace: reformat current text
429 # if all we have after the cursor is whitespace: reformat current text
428 # before cursor
430 # before cursor
429 after_cursor = d.text[d.cursor_position :]
431 after_cursor = d.text[d.cursor_position :]
430 reformatted = False
432 reformatted = False
431 if not after_cursor.strip():
433 if not after_cursor.strip():
432 reformat_text_before_cursor(b, d, shell)
434 reformat_text_before_cursor(b, d, shell)
433 reformatted = True
435 reformatted = True
434 if not (
436 if not (
435 d.on_last_line
437 d.on_last_line
436 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
438 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
437 ):
439 ):
438 if shell.autoindent:
440 if shell.autoindent:
439 b.insert_text("\n" + indent)
441 b.insert_text("\n" + indent)
440 else:
442 else:
441 b.insert_text("\n")
443 b.insert_text("\n")
442 return
444 return
443
445
444 if (status != "incomplete") and b.accept_handler:
446 if (status != "incomplete") and b.accept_handler:
445 if not reformatted:
447 if not reformatted:
446 reformat_text_before_cursor(b, d, shell)
448 reformat_text_before_cursor(b, d, shell)
447 b.validate_and_handle()
449 b.validate_and_handle()
448 else:
450 else:
449 if shell.autoindent:
451 if shell.autoindent:
450 b.insert_text("\n" + indent)
452 b.insert_text("\n" + indent)
451 else:
453 else:
452 b.insert_text("\n")
454 b.insert_text("\n")
453
455
454 return newline_or_execute
456 return newline_or_execute
455
457
456
458
457 def previous_history_or_previous_completion(event):
459 def previous_history_or_previous_completion(event):
458 """
460 """
459 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
461 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
460
462
461 If completer is open this still select previous completion.
463 If completer is open this still select previous completion.
462 """
464 """
463 event.current_buffer.auto_up()
465 event.current_buffer.auto_up()
464
466
465
467
466 def next_history_or_next_completion(event):
468 def next_history_or_next_completion(event):
467 """
469 """
468 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
470 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
469
471
470 If completer is open this still select next completion.
472 If completer is open this still select next completion.
471 """
473 """
472 event.current_buffer.auto_down()
474 event.current_buffer.auto_down()
473
475
474
476
475 def dismiss_completion(event):
477 def dismiss_completion(event):
476 """Dismiss completion"""
478 """Dismiss completion"""
477 b = event.current_buffer
479 b = event.current_buffer
478 if b.complete_state:
480 if b.complete_state:
479 b.cancel_completion()
481 b.cancel_completion()
480
482
481
483
482 def reset_buffer(event):
484 def reset_buffer(event):
483 """Reset buffer"""
485 """Reset buffer"""
484 b = event.current_buffer
486 b = event.current_buffer
485 if b.complete_state:
487 if b.complete_state:
486 b.cancel_completion()
488 b.cancel_completion()
487 else:
489 else:
488 b.reset()
490 b.reset()
489
491
490
492
491 def reset_search_buffer(event):
493 def reset_search_buffer(event):
492 """Reset search buffer"""
494 """Reset search buffer"""
493 if event.current_buffer.document.text:
495 if event.current_buffer.document.text:
494 event.current_buffer.reset()
496 event.current_buffer.reset()
495 else:
497 else:
496 event.app.layout.focus(DEFAULT_BUFFER)
498 event.app.layout.focus(DEFAULT_BUFFER)
497
499
498
500
499 def suspend_to_bg(event):
501 def suspend_to_bg(event):
500 """Suspend to background"""
502 """Suspend to background"""
501 event.app.suspend_to_background()
503 event.app.suspend_to_background()
502
504
503
505
504 def quit(event):
506 def quit(event):
505 """
507 """
506 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
508 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
507
509
508 On platforms that support SIGQUIT, send SIGQUIT to the current process.
510 On platforms that support SIGQUIT, send SIGQUIT to the current process.
509 On other platforms, just exit the process with a message.
511 On other platforms, just exit the process with a message.
510 """
512 """
511 sigquit = getattr(signal, "SIGQUIT", None)
513 sigquit = getattr(signal, "SIGQUIT", None)
512 if sigquit is not None:
514 if sigquit is not None:
513 os.kill(0, signal.SIGQUIT)
515 os.kill(0, signal.SIGQUIT)
514 else:
516 else:
515 sys.exit("Quit")
517 sys.exit("Quit")
516
518
517
519
518 def indent_buffer(event):
520 def indent_buffer(event):
519 """Indent buffer"""
521 """Indent buffer"""
520 event.current_buffer.insert_text(" " * 4)
522 event.current_buffer.insert_text(" " * 4)
521
523
522
524
523 def newline_autoindent(event):
525 def newline_autoindent(event):
524 """Insert a newline after the cursor indented appropriately.
526 """Insert a newline after the cursor indented appropriately.
525
527
526 Fancier version of former ``newline_with_copy_margin`` which should
528 Fancier version of former ``newline_with_copy_margin`` which should
527 compute the correct indentation of the inserted line. That is to say, indent
529 compute the correct indentation of the inserted line. That is to say, indent
528 by 4 extra space after a function definition, class definition, context
530 by 4 extra space after a function definition, class definition, context
529 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
531 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
530 """
532 """
531 shell = get_ipython()
533 shell = get_ipython()
532 inputsplitter = shell.input_transformer_manager
534 inputsplitter = shell.input_transformer_manager
533 b = event.current_buffer
535 b = event.current_buffer
534 d = b.document
536 d = b.document
535
537
536 if b.complete_state:
538 if b.complete_state:
537 b.cancel_completion()
539 b.cancel_completion()
538 text = d.text[: d.cursor_position] + "\n"
540 text = d.text[: d.cursor_position] + "\n"
539 _, indent = inputsplitter.check_complete(text)
541 _, indent = inputsplitter.check_complete(text)
540 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
542 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
541
543
542
544
543 def open_input_in_editor(event):
545 def open_input_in_editor(event):
544 """Open code from input in external editor"""
546 """Open code from input in external editor"""
545 event.app.current_buffer.open_in_editor()
547 event.app.current_buffer.open_in_editor()
546
548
547
549
548 if sys.platform == "win32":
550 if sys.platform == "win32":
549 from IPython.core.error import TryNext
551 from IPython.core.error import TryNext
550 from IPython.lib.clipboard import (
552 from IPython.lib.clipboard import (
551 ClipboardEmpty,
553 ClipboardEmpty,
552 tkinter_clipboard_get,
554 tkinter_clipboard_get,
553 win32_clipboard_get,
555 win32_clipboard_get,
554 )
556 )
555
557
556 @undoc
558 @undoc
557 def win_paste(event):
559 def win_paste(event):
558 try:
560 try:
559 text = win32_clipboard_get()
561 text = win32_clipboard_get()
560 except TryNext:
562 except TryNext:
561 try:
563 try:
562 text = tkinter_clipboard_get()
564 text = tkinter_clipboard_get()
563 except (TryNext, ClipboardEmpty):
565 except (TryNext, ClipboardEmpty):
564 return
566 return
565 except ClipboardEmpty:
567 except ClipboardEmpty:
566 return
568 return
567 event.current_buffer.insert_text(text.replace("\t", " " * 4))
569 event.current_buffer.insert_text(text.replace("\t", " " * 4))
568
570
569 else:
571 else:
570
572
571 @undoc
573 @undoc
572 def win_paste(event):
574 def win_paste(event):
573 """Stub used on other platforms"""
575 """Stub used on other platforms"""
574 pass
576 pass
575
577
576
578
577 KEY_BINDINGS = [
579 KEY_BINDINGS = [
578 Binding(
580 Binding(
579 handle_return_or_newline_or_execute,
581 handle_return_or_newline_or_execute,
580 ["enter"],
582 ["enter"],
581 "default_buffer_focused & ~has_selection & insert_mode",
583 "default_buffer_focused & ~has_selection & insert_mode",
582 ),
584 ),
583 Binding(
585 Binding(
584 reformat_and_execute,
586 reformat_and_execute,
585 ["escape", "enter"],
587 ["escape", "enter"],
586 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
588 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
587 ),
589 ),
588 Binding(quit, ["c-\\"]),
590 Binding(quit, ["c-\\"]),
589 Binding(
591 Binding(
590 previous_history_or_previous_completion,
592 previous_history_or_previous_completion,
591 ["c-p"],
593 ["c-p"],
592 "vi_insert_mode & default_buffer_focused",
594 "vi_insert_mode & default_buffer_focused",
593 ),
595 ),
594 Binding(
596 Binding(
595 next_history_or_next_completion,
597 next_history_or_next_completion,
596 ["c-n"],
598 ["c-n"],
597 "vi_insert_mode & default_buffer_focused",
599 "vi_insert_mode & default_buffer_focused",
598 ),
600 ),
599 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
601 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
600 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
602 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
601 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
603 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
602 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
604 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
603 Binding(
605 Binding(
604 indent_buffer,
606 indent_buffer,
605 ["tab"], # Ctrl+I == Tab
607 ["tab"], # Ctrl+I == Tab
606 "default_buffer_focused"
608 "default_buffer_focused"
607 " & ~has_selection"
609 " & ~has_selection"
608 " & insert_mode"
610 " & insert_mode"
609 " & cursor_in_leading_ws",
611 " & cursor_in_leading_ws",
610 ),
612 ),
611 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
613 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
612 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
614 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
613 *AUTO_MATCH_BINDINGS,
615 *AUTO_MATCH_BINDINGS,
614 *AUTO_SUGGEST_BINDINGS,
616 *AUTO_SUGGEST_BINDINGS,
615 Binding(
617 Binding(
616 display_completions_like_readline,
618 display_completions_like_readline,
617 ["c-i"],
619 ["c-i"],
618 "readline_like_completions"
620 "readline_like_completions"
619 " & default_buffer_focused"
621 " & default_buffer_focused"
620 " & ~has_selection"
622 " & ~has_selection"
621 " & insert_mode"
623 " & insert_mode"
622 " & ~cursor_in_leading_ws",
624 " & ~cursor_in_leading_ws",
623 ),
625 ),
624 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
626 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
625 *SIMPLE_CONTROL_BINDINGS,
627 *SIMPLE_CONTROL_BINDINGS,
626 *ALT_AND_COMOBO_CONTROL_BINDINGS,
628 *ALT_AND_COMOBO_CONTROL_BINDINGS,
627 ]
629 ]
@@ -1,282 +1,290 b''
1 """
1 """
2 Filters restricting scope of IPython Terminal shortcuts.
2 Filters restricting scope of IPython Terminal shortcuts.
3 """
3 """
4
4
5 # Copyright (c) IPython Development Team.
5 # Copyright (c) IPython Development Team.
6 # Distributed under the terms of the Modified BSD License.
6 # Distributed under the terms of the Modified BSD License.
7
7
8 import ast
8 import ast
9 import re
9 import re
10 import signal
10 import signal
11 import sys
11 import sys
12 from typing import Callable, Dict, Union
12 from typing import Callable, Dict, Union
13
13
14 from prompt_toolkit.application.current import get_app
14 from prompt_toolkit.application.current import get_app
15 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
15 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
16 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
16 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
17 from prompt_toolkit.filters import has_focus as has_focus_impl
17 from prompt_toolkit.filters import has_focus as has_focus_impl
18 from prompt_toolkit.filters import (
18 from prompt_toolkit.filters import (
19 Always,
19 Always,
20 Never,
20 Never,
21 has_selection,
21 has_selection,
22 has_suggestion,
22 has_suggestion,
23 vi_insert_mode,
23 vi_insert_mode,
24 vi_mode,
24 vi_mode,
25 )
25 )
26 from prompt_toolkit.layout.layout import FocusableElement
26 from prompt_toolkit.layout.layout import FocusableElement
27
27
28 from IPython.core.getipython import get_ipython
28 from IPython.core.getipython import get_ipython
29 from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
29 from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
30 from IPython.terminal.shortcuts import auto_suggest
30 from IPython.terminal.shortcuts import auto_suggest
31 from IPython.utils.decorators import undoc
31 from IPython.utils.decorators import undoc
32
32
33
33
34 @undoc
34 @undoc
35 @Condition
35 @Condition
36 def cursor_in_leading_ws():
36 def cursor_in_leading_ws():
37 before = get_app().current_buffer.document.current_line_before_cursor
37 before = get_app().current_buffer.document.current_line_before_cursor
38 return (not before) or before.isspace()
38 return (not before) or before.isspace()
39
39
40
40
41 def has_focus(value: FocusableElement):
41 def has_focus(value: FocusableElement):
42 """Wrapper around has_focus adding a nice `__name__` to tester function"""
42 """Wrapper around has_focus adding a nice `__name__` to tester function"""
43 tester = has_focus_impl(value).func
43 tester = has_focus_impl(value).func
44 tester.__name__ = f"is_focused({value})"
44 tester.__name__ = f"is_focused({value})"
45 return Condition(tester)
45 return Condition(tester)
46
46
47
47
48 @undoc
48 @undoc
49 @Condition
49 @Condition
50 def has_line_below() -> bool:
50 def has_line_below() -> bool:
51 document = get_app().current_buffer.document
51 document = get_app().current_buffer.document
52 return document.cursor_position_row < len(document.lines) - 1
52 return document.cursor_position_row < len(document.lines) - 1
53
53
54
54
55 @undoc
55 @undoc
56 @Condition
56 @Condition
57 def is_cursor_at_the_end_of_line() -> bool:
58 document = get_app().current_buffer.document
59 return document.is_cursor_at_the_end_of_line
60
61
62 @undoc
63 @Condition
57 def has_line_above() -> bool:
64 def has_line_above() -> bool:
58 document = get_app().current_buffer.document
65 document = get_app().current_buffer.document
59 return document.cursor_position_row != 0
66 return document.cursor_position_row != 0
60
67
61
68
62 @Condition
69 @Condition
63 def ebivim():
70 def ebivim():
64 shell = get_ipython()
71 shell = get_ipython()
65 return shell.emacs_bindings_in_vi_insert_mode
72 return shell.emacs_bindings_in_vi_insert_mode
66
73
67
74
68 @Condition
75 @Condition
69 def supports_suspend():
76 def supports_suspend():
70 return hasattr(signal, "SIGTSTP")
77 return hasattr(signal, "SIGTSTP")
71
78
72
79
73 @Condition
80 @Condition
74 def auto_match():
81 def auto_match():
75 shell = get_ipython()
82 shell = get_ipython()
76 return shell.auto_match
83 return shell.auto_match
77
84
78
85
79 def all_quotes_paired(quote, buf):
86 def all_quotes_paired(quote, buf):
80 paired = True
87 paired = True
81 i = 0
88 i = 0
82 while i < len(buf):
89 while i < len(buf):
83 c = buf[i]
90 c = buf[i]
84 if c == quote:
91 if c == quote:
85 paired = not paired
92 paired = not paired
86 elif c == "\\":
93 elif c == "\\":
87 i += 1
94 i += 1
88 i += 1
95 i += 1
89 return paired
96 return paired
90
97
91
98
92 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
99 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
93 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
100 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
94
101
95
102
96 def preceding_text(pattern: Union[str, Callable]):
103 def preceding_text(pattern: Union[str, Callable]):
97 if pattern in _preceding_text_cache:
104 if pattern in _preceding_text_cache:
98 return _preceding_text_cache[pattern]
105 return _preceding_text_cache[pattern]
99
106
100 if callable(pattern):
107 if callable(pattern):
101
108
102 def _preceding_text():
109 def _preceding_text():
103 app = get_app()
110 app = get_app()
104 before_cursor = app.current_buffer.document.current_line_before_cursor
111 before_cursor = app.current_buffer.document.current_line_before_cursor
105 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
112 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
106 return bool(pattern(before_cursor)) # type: ignore[operator]
113 return bool(pattern(before_cursor)) # type: ignore[operator]
107
114
108 else:
115 else:
109 m = re.compile(pattern)
116 m = re.compile(pattern)
110
117
111 def _preceding_text():
118 def _preceding_text():
112 app = get_app()
119 app = get_app()
113 before_cursor = app.current_buffer.document.current_line_before_cursor
120 before_cursor = app.current_buffer.document.current_line_before_cursor
114 return bool(m.match(before_cursor))
121 return bool(m.match(before_cursor))
115
122
116 _preceding_text.__name__ = f"preceding_text({pattern!r})"
123 _preceding_text.__name__ = f"preceding_text({pattern!r})"
117
124
118 condition = Condition(_preceding_text)
125 condition = Condition(_preceding_text)
119 _preceding_text_cache[pattern] = condition
126 _preceding_text_cache[pattern] = condition
120 return condition
127 return condition
121
128
122
129
123 def following_text(pattern):
130 def following_text(pattern):
124 try:
131 try:
125 return _following_text_cache[pattern]
132 return _following_text_cache[pattern]
126 except KeyError:
133 except KeyError:
127 pass
134 pass
128 m = re.compile(pattern)
135 m = re.compile(pattern)
129
136
130 def _following_text():
137 def _following_text():
131 app = get_app()
138 app = get_app()
132 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
139 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
133
140
134 _following_text.__name__ = f"following_text({pattern!r})"
141 _following_text.__name__ = f"following_text({pattern!r})"
135
142
136 condition = Condition(_following_text)
143 condition = Condition(_following_text)
137 _following_text_cache[pattern] = condition
144 _following_text_cache[pattern] = condition
138 return condition
145 return condition
139
146
140
147
141 @Condition
148 @Condition
142 def not_inside_unclosed_string():
149 def not_inside_unclosed_string():
143 app = get_app()
150 app = get_app()
144 s = app.current_buffer.document.text_before_cursor
151 s = app.current_buffer.document.text_before_cursor
145 # remove escaped quotes
152 # remove escaped quotes
146 s = s.replace('\\"', "").replace("\\'", "")
153 s = s.replace('\\"', "").replace("\\'", "")
147 # remove triple-quoted string literals
154 # remove triple-quoted string literals
148 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
155 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
149 # remove single-quoted string literals
156 # remove single-quoted string literals
150 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
157 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
151 return not ('"' in s or "'" in s)
158 return not ('"' in s or "'" in s)
152
159
153
160
154 @Condition
161 @Condition
155 def navigable_suggestions():
162 def navigable_suggestions():
156 shell = get_ipython()
163 shell = get_ipython()
157 return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
164 return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
158
165
159
166
160 @Condition
167 @Condition
161 def readline_like_completions():
168 def readline_like_completions():
162 shell = get_ipython()
169 shell = get_ipython()
163 return shell.display_completions == "readlinelike"
170 return shell.display_completions == "readlinelike"
164
171
165
172
166 @Condition
173 @Condition
167 def is_windows_os():
174 def is_windows_os():
168 return sys.platform == "win32"
175 return sys.platform == "win32"
169
176
170
177
171 # these one is callable and re-used multiple times hence needs to be
178 # these one is callable and re-used multiple times hence needs to be
172 # only defined once beforhand so that transforming back to human-readable
179 # only defined once beforhand so that transforming back to human-readable
173 # names works well in the documentation.
180 # names works well in the documentation.
174 default_buffer_focused = has_focus(DEFAULT_BUFFER)
181 default_buffer_focused = has_focus(DEFAULT_BUFFER)
175
182
176 KEYBINDING_FILTERS = {
183 KEYBINDING_FILTERS = {
177 "always": Always(),
184 "always": Always(),
178 # never is used for exposing commands which have no default keybindings
185 # never is used for exposing commands which have no default keybindings
179 "never": Never(),
186 "never": Never(),
180 "has_line_below": has_line_below,
187 "has_line_below": has_line_below,
181 "has_line_above": has_line_above,
188 "has_line_above": has_line_above,
189 "is_cursor_at_the_end_of_line": is_cursor_at_the_end_of_line,
182 "has_selection": has_selection,
190 "has_selection": has_selection,
183 "has_suggestion": has_suggestion,
191 "has_suggestion": has_suggestion,
184 "vi_mode": vi_mode,
192 "vi_mode": vi_mode,
185 "vi_insert_mode": vi_insert_mode,
193 "vi_insert_mode": vi_insert_mode,
186 "emacs_insert_mode": emacs_insert_mode,
194 "emacs_insert_mode": emacs_insert_mode,
187 # https://github.com/ipython/ipython/pull/12603 argued for inclusion of
195 # https://github.com/ipython/ipython/pull/12603 argued for inclusion of
188 # emacs key bindings with a configurable `emacs_bindings_in_vi_insert_mode`
196 # emacs key bindings with a configurable `emacs_bindings_in_vi_insert_mode`
189 # toggle; when the toggle is on user can access keybindigns like `ctrl + e`
197 # toggle; when the toggle is on user can access keybindigns like `ctrl + e`
190 # in vi insert mode. Because some of the emacs bindings involve `escape`
198 # in vi insert mode. Because some of the emacs bindings involve `escape`
191 # followed by another key, e.g. `escape` followed by `f`, prompt-toolkit
199 # followed by another key, e.g. `escape` followed by `f`, prompt-toolkit
192 # needs to wait to see if there will be another character typed in before
200 # needs to wait to see if there will be another character typed in before
193 # executing pure `escape` keybinding; in vi insert mode `escape` switches to
201 # executing pure `escape` keybinding; in vi insert mode `escape` switches to
194 # command mode which is common and performance critical action for vi users.
202 # command mode which is common and performance critical action for vi users.
195 # To avoid the delay users employ a workaround:
203 # To avoid the delay users employ a workaround:
196 # https://github.com/ipython/ipython/issues/13443#issuecomment-1032753703
204 # https://github.com/ipython/ipython/issues/13443#issuecomment-1032753703
197 # which involves switching `emacs_bindings_in_vi_insert_mode` off.
205 # which involves switching `emacs_bindings_in_vi_insert_mode` off.
198 #
206 #
199 # For the workaround to work:
207 # For the workaround to work:
200 # 1) end users need to toggle `emacs_bindings_in_vi_insert_mode` off
208 # 1) end users need to toggle `emacs_bindings_in_vi_insert_mode` off
201 # 2) all keybindings which would involve `escape` need to respect that
209 # 2) all keybindings which would involve `escape` need to respect that
202 # toggle by including either:
210 # toggle by including either:
203 # - `vi_insert_mode & ebivim` for actions which have emacs keybindings
211 # - `vi_insert_mode & ebivim` for actions which have emacs keybindings
204 # predefined upstream in prompt-toolkit, or
212 # predefined upstream in prompt-toolkit, or
205 # - `emacs_like_insert_mode` for actions which do not have existing
213 # - `emacs_like_insert_mode` for actions which do not have existing
206 # emacs keybindings predefined upstream (or need overriding of the
214 # emacs keybindings predefined upstream (or need overriding of the
207 # upstream bindings to modify behaviour), defined below.
215 # upstream bindings to modify behaviour), defined below.
208 "emacs_like_insert_mode": (vi_insert_mode & ebivim) | emacs_insert_mode,
216 "emacs_like_insert_mode": (vi_insert_mode & ebivim) | emacs_insert_mode,
209 "has_completions": has_completions,
217 "has_completions": has_completions,
210 "insert_mode": vi_insert_mode | emacs_insert_mode,
218 "insert_mode": vi_insert_mode | emacs_insert_mode,
211 "default_buffer_focused": default_buffer_focused,
219 "default_buffer_focused": default_buffer_focused,
212 "search_buffer_focused": has_focus(SEARCH_BUFFER),
220 "search_buffer_focused": has_focus(SEARCH_BUFFER),
213 # `ebivim` stands for emacs bindings in vi insert mode
221 # `ebivim` stands for emacs bindings in vi insert mode
214 "ebivim": ebivim,
222 "ebivim": ebivim,
215 "supports_suspend": supports_suspend,
223 "supports_suspend": supports_suspend,
216 "is_windows_os": is_windows_os,
224 "is_windows_os": is_windows_os,
217 "auto_match": auto_match,
225 "auto_match": auto_match,
218 "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
226 "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
219 "not_inside_unclosed_string": not_inside_unclosed_string,
227 "not_inside_unclosed_string": not_inside_unclosed_string,
220 "readline_like_completions": readline_like_completions,
228 "readline_like_completions": readline_like_completions,
221 "preceded_by_paired_double_quotes": preceding_text(
229 "preceded_by_paired_double_quotes": preceding_text(
222 lambda line: all_quotes_paired('"', line)
230 lambda line: all_quotes_paired('"', line)
223 ),
231 ),
224 "preceded_by_paired_single_quotes": preceding_text(
232 "preceded_by_paired_single_quotes": preceding_text(
225 lambda line: all_quotes_paired("'", line)
233 lambda line: all_quotes_paired("'", line)
226 ),
234 ),
227 "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
235 "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
228 "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
236 "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
229 "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
237 "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
230 "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
238 "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
231 "preceded_by_opening_round_paren": preceding_text(r".*\($"),
239 "preceded_by_opening_round_paren": preceding_text(r".*\($"),
232 "preceded_by_opening_bracket": preceding_text(r".*\[$"),
240 "preceded_by_opening_bracket": preceding_text(r".*\[$"),
233 "preceded_by_opening_brace": preceding_text(r".*\{$"),
241 "preceded_by_opening_brace": preceding_text(r".*\{$"),
234 "preceded_by_double_quote": preceding_text('.*"$'),
242 "preceded_by_double_quote": preceding_text('.*"$'),
235 "preceded_by_single_quote": preceding_text(r".*'$"),
243 "preceded_by_single_quote": preceding_text(r".*'$"),
236 "followed_by_closing_round_paren": following_text(r"^\)"),
244 "followed_by_closing_round_paren": following_text(r"^\)"),
237 "followed_by_closing_bracket": following_text(r"^\]"),
245 "followed_by_closing_bracket": following_text(r"^\]"),
238 "followed_by_closing_brace": following_text(r"^\}"),
246 "followed_by_closing_brace": following_text(r"^\}"),
239 "followed_by_double_quote": following_text('^"'),
247 "followed_by_double_quote": following_text('^"'),
240 "followed_by_single_quote": following_text("^'"),
248 "followed_by_single_quote": following_text("^'"),
241 "navigable_suggestions": navigable_suggestions,
249 "navigable_suggestions": navigable_suggestions,
242 "cursor_in_leading_ws": cursor_in_leading_ws,
250 "cursor_in_leading_ws": cursor_in_leading_ws,
243 }
251 }
244
252
245
253
246 def eval_node(node: Union[ast.AST, None]):
254 def eval_node(node: Union[ast.AST, None]):
247 if node is None:
255 if node is None:
248 return None
256 return None
249 if isinstance(node, ast.Expression):
257 if isinstance(node, ast.Expression):
250 return eval_node(node.body)
258 return eval_node(node.body)
251 if isinstance(node, ast.BinOp):
259 if isinstance(node, ast.BinOp):
252 left = eval_node(node.left)
260 left = eval_node(node.left)
253 right = eval_node(node.right)
261 right = eval_node(node.right)
254 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
262 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
255 if dunders:
263 if dunders:
256 return getattr(left, dunders[0])(right)
264 return getattr(left, dunders[0])(right)
257 raise ValueError(f"Unknown binary operation: {node.op}")
265 raise ValueError(f"Unknown binary operation: {node.op}")
258 if isinstance(node, ast.UnaryOp):
266 if isinstance(node, ast.UnaryOp):
259 value = eval_node(node.operand)
267 value = eval_node(node.operand)
260 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
268 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
261 if dunders:
269 if dunders:
262 return getattr(value, dunders[0])()
270 return getattr(value, dunders[0])()
263 raise ValueError(f"Unknown unary operation: {node.op}")
271 raise ValueError(f"Unknown unary operation: {node.op}")
264 if isinstance(node, ast.Name):
272 if isinstance(node, ast.Name):
265 if node.id in KEYBINDING_FILTERS:
273 if node.id in KEYBINDING_FILTERS:
266 return KEYBINDING_FILTERS[node.id]
274 return KEYBINDING_FILTERS[node.id]
267 else:
275 else:
268 sep = "\n - "
276 sep = "\n - "
269 known_filters = sep.join(sorted(KEYBINDING_FILTERS))
277 known_filters = sep.join(sorted(KEYBINDING_FILTERS))
270 raise NameError(
278 raise NameError(
271 f"{node.id} is not a known shortcut filter."
279 f"{node.id} is not a known shortcut filter."
272 f" Known filters are: {sep}{known_filters}."
280 f" Known filters are: {sep}{known_filters}."
273 )
281 )
274 raise ValueError("Unhandled node", ast.dump(node))
282 raise ValueError("Unhandled node", ast.dump(node))
275
283
276
284
277 def filter_from_string(code: str):
285 def filter_from_string(code: str):
278 expression = ast.parse(code, mode="eval")
286 expression = ast.parse(code, mode="eval")
279 return eval_node(expression)
287 return eval_node(expression)
280
288
281
289
282 __all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
290 __all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
General Comments 0
You need to be logged in to leave comments. Login now