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