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