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