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