##// END OF EJS Templates
Implement traversal of autosuggestions and by-character fill
krassowski -
Show More
@@ -0,0 +1,255 b''
1 import re
2 import tokenize
3 from io import StringIO
4 from typing import Callable, List, Optional, Union
5
6 from prompt_toolkit.buffer import Buffer
7 from prompt_toolkit.key_binding import KeyPressEvent
8 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 from prompt_toolkit.document import Document
11 from prompt_toolkit.history import History
12 from prompt_toolkit.shortcuts import PromptSession
13
14 from IPython.utils.tokenutil import generate_tokens
15
16
17 def _get_query(document: Document):
18 return document.text.rsplit("\n", 1)[-1]
19
20
21 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
22 """ """
23
24 def __init__(
25 self,
26 ):
27 self.skip_lines = 0
28 self._connected_apps = []
29
30 def reset_history_position(self, _: Buffer):
31 self.skip_lines = 0
32
33 def disconnect(self):
34 for pt_app in self._connected_apps:
35 text_insert_event = pt_app.default_buffer.on_text_insert
36 text_insert_event.remove_handler(self.reset_history_position)
37
38 def connect(self, pt_app: PromptSession):
39 self._connected_apps.append(pt_app)
40 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
41
42 def get_suggestion(
43 self, buffer: Buffer, document: Document
44 ) -> Optional[Suggestion]:
45 text = _get_query(document)
46
47 if text.strip():
48 for suggestion, _ in self._find_next_match(
49 text, self.skip_lines, buffer.history
50 ):
51 return Suggestion(suggestion)
52
53 return None
54
55 def _find_match(
56 self, text: str, skip_lines: float, history: History, previous: bool
57 ):
58 line_number = -1
59
60 for string in reversed(list(history.get_strings())):
61 for line in reversed(string.splitlines()):
62 line_number += 1
63 if not previous and line_number < skip_lines:
64 continue
65 # do not return empty suggestions as these
66 # close the auto-suggestion overlay (and are useless)
67 if line.startswith(text) and len(line) > len(text):
68 yield line[len(text) :], line_number
69 if previous and line_number >= skip_lines:
70 return
71
72 def _find_next_match(self, text: str, skip_lines: float, history: History):
73 return self._find_match(text, skip_lines, history, previous=False)
74
75 def _find_previous_match(self, text: str, skip_lines: float, history: History):
76 return reversed(
77 list(self._find_match(text, skip_lines, history, previous=True))
78 )
79
80 def up(self, query: str, other_than: str, history: History):
81 for suggestion, line_number in self._find_next_match(
82 query, self.skip_lines, history
83 ):
84 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
85 # we want to switch from 'very.b' to 'very.a' because a) if they
86 # suggestion equals current text, prompt-toolit aborts suggesting
87 # b) user likely would not be interested in 'very' anyways (they
88 # already typed it).
89 if query + suggestion != other_than:
90 self.skip_lines = line_number
91 break
92 else:
93 # no matches found, cycle back to beginning
94 self.skip_lines = 0
95
96 def down(self, query: str, other_than: str, history: History):
97 for suggestion, line_number in self._find_previous_match(
98 query, self.skip_lines, history
99 ):
100 if query + suggestion != other_than:
101 self.skip_lines = line_number
102 break
103 else:
104 # no matches found, cycle to end
105 for suggestion, line_number in self._find_previous_match(
106 query, float("Inf"), history
107 ):
108 if query + suggestion != other_than:
109 self.skip_lines = line_number
110 break
111
112
113 # Needed for to accept autosuggestions in vi insert mode
114 def accept_in_vi_insert_mode(event: KeyPressEvent):
115 """Apply autosuggestion if at end of line."""
116 b = event.current_buffer
117 d = b.document
118 after_cursor = d.text[d.cursor_position :]
119 lines = after_cursor.split("\n")
120 end_of_current_line = lines[0].strip()
121 suggestion = b.suggestion
122 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
123 b.insert_text(suggestion.text)
124 else:
125 nc.end_of_line(event)
126
127
128 def accept(event: KeyPressEvent):
129 """Accept autosuggestion"""
130 b = event.current_buffer
131 suggestion = b.suggestion
132 if suggestion:
133 b.insert_text(suggestion.text)
134 else:
135 nc.forward_char(event)
136
137
138 def accept_word(event: KeyPressEvent):
139 """Fill partial autosuggestion by word"""
140 b = event.current_buffer
141 suggestion = b.suggestion
142 if suggestion:
143 t = re.split(r"(\S+\s+)", suggestion.text)
144 b.insert_text(next((x for x in t if x), ""))
145 else:
146 nc.forward_word(event)
147
148
149 def accept_character(event: KeyPressEvent):
150 """Fill partial autosuggestion by character"""
151 b = event.current_buffer
152 suggestion = b.suggestion
153 if suggestion and suggestion.text:
154 b.insert_text(suggestion.text[0])
155
156
157 def accept_token(event: KeyPressEvent):
158 """Fill partial autosuggestion by token"""
159 b = event.current_buffer
160 suggestion = b.suggestion
161
162 if suggestion:
163 prefix = _get_query(b.document)
164 text = prefix + suggestion.text
165
166 tokens: List[Optional[str]] = [None, None, None]
167 substrings = [""]
168 i = 0
169
170 for token in generate_tokens(StringIO(text).readline):
171 if token.type == tokenize.NEWLINE:
172 index = len(text)
173 else:
174 index = text.index(token[1], len(substrings[-1]))
175 substrings.append(text[:index])
176 tokenized_so_far = substrings[-1]
177 if tokenized_so_far.startswith(prefix):
178 if i == 0 and len(tokenized_so_far) > len(prefix):
179 tokens[0] = tokenized_so_far[len(prefix) :]
180 substrings.append(tokenized_so_far)
181 i += 1
182 tokens[i] = token[1]
183 if i == 2:
184 break
185 i += 1
186
187 if tokens[0]:
188 to_insert: str
189 insert_text = substrings[-2]
190 if tokens[1] and len(tokens[1]) == 1:
191 insert_text = substrings[-1]
192 to_insert = insert_text[len(prefix) :]
193 b.insert_text(to_insert)
194 return
195
196 nc.forward_word(event)
197
198
199 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
200
201
202 def _swap_autosuggestion(
203 buffer: Buffer,
204 provider: NavigableAutoSuggestFromHistory,
205 direction_method: Callable,
206 ):
207 """
208 We skip most recent history entry (in either direction) if it equals the
209 current autosuggestion because if user cycles when auto-suggestion is shown
210 they most likely want something else than what was suggested (othewrise
211 they would have accepted the suggestion).
212 """
213 suggestion = buffer.suggestion
214 if not suggestion:
215 return
216
217 query = _get_query(buffer.document)
218 current = query + suggestion.text
219
220 direction_method(query=query, other_than=current, history=buffer.history)
221
222 new_suggestion = provider.get_suggestion(buffer, buffer.document)
223 buffer.suggestion = new_suggestion
224
225
226 def swap_autosuggestion_up(provider: Provider):
227 def swap_autosuggestion_up(event: KeyPressEvent):
228 """Get next autosuggestion from history."""
229 if not isinstance(provider, NavigableAutoSuggestFromHistory):
230 return
231
232 return _swap_autosuggestion(
233 buffer=event.current_buffer, provider=provider, direction_method=provider.up
234 )
235
236 swap_autosuggestion_up.__name__ = "swap_autosuggestion_up"
237 return swap_autosuggestion_up
238
239
240 def swap_autosuggestion_down(
241 provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
242 ):
243 def swap_autosuggestion_down(event: KeyPressEvent):
244 """Get previous autosuggestion from history."""
245 if not isinstance(provider, NavigableAutoSuggestFromHistory):
246 return
247
248 return _swap_autosuggestion(
249 buffer=event.current_buffer,
250 provider=provider,
251 direction_method=provider.down,
252 )
253
254 swap_autosuggestion_down.__name__ = "swap_autosuggestion_down"
255 return swap_autosuggestion_down
@@ -4,6 +4,7 b' import asyncio'
4 import os
4 import os
5 import sys
5 import sys
6 from warnings import warn
6 from warnings import warn
7 from typing import Union as UnionType
7
8
8 from IPython.core.async_helpers import get_asyncio_loop
9 from IPython.core.async_helpers import get_asyncio_loop
9 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
10 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
@@ -49,6 +50,7 b' from .pt_inputhooks import get_inputhook_name_and_func'
49 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
50 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
50 from .ptutils import IPythonPTCompleter, IPythonPTLexer
51 from .ptutils import IPythonPTCompleter, IPythonPTLexer
51 from .shortcuts import create_ipython_shortcuts
52 from .shortcuts import create_ipython_shortcuts
53 from .shortcuts.auto_suggest import NavigableAutoSuggestFromHistory
52
54
53 PTK3 = ptk_version.startswith('3.')
55 PTK3 = ptk_version.startswith('3.')
54
56
@@ -183,7 +185,7 b' class TerminalInteractiveShell(InteractiveShell):'
183 'menus, decrease for short and wide.'
185 'menus, decrease for short and wide.'
184 ).tag(config=True)
186 ).tag(config=True)
185
187
186 pt_app = None
188 pt_app: UnionType[PromptSession, None] = None
187 debugger_history = None
189 debugger_history = None
188
190
189 debugger_history_file = Unicode(
191 debugger_history_file = Unicode(
@@ -376,18 +378,25 b' class TerminalInteractiveShell(InteractiveShell):'
376 ).tag(config=True)
378 ).tag(config=True)
377
379
378 autosuggestions_provider = Unicode(
380 autosuggestions_provider = Unicode(
379 "AutoSuggestFromHistory",
381 "NavigableAutoSuggestFromHistory",
380 help="Specifies from which source automatic suggestions are provided. "
382 help="Specifies from which source automatic suggestions are provided. "
381 "Can be set to `'AutoSuggestFromHistory`' or `None` to disable"
383 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
382 "automatic suggestions. Default is `'AutoSuggestFromHistory`'.",
384 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
385 " or ``None`` to disable automatic suggestions. "
386 "Default is `'NavigableAutoSuggestFromHistory`'.",
383 allow_none=True,
387 allow_none=True,
384 ).tag(config=True)
388 ).tag(config=True)
385
389
386 def _set_autosuggestions(self, provider):
390 def _set_autosuggestions(self, provider):
391 # disconnect old handler
392 if self.auto_suggest and isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
393 self.auto_suggest.disconnect()
387 if provider is None:
394 if provider is None:
388 self.auto_suggest = None
395 self.auto_suggest = None
389 elif provider == "AutoSuggestFromHistory":
396 elif provider == "AutoSuggestFromHistory":
390 self.auto_suggest = AutoSuggestFromHistory()
397 self.auto_suggest = AutoSuggestFromHistory()
398 elif provider == "NavigableAutoSuggestFromHistory":
399 self.auto_suggest = NavigableAutoSuggestFromHistory()
391 else:
400 else:
392 raise ValueError("No valid provider.")
401 raise ValueError("No valid provider.")
393 if self.pt_app:
402 if self.pt_app:
@@ -462,6 +471,8 b' class TerminalInteractiveShell(InteractiveShell):'
462 tempfile_suffix=".py",
471 tempfile_suffix=".py",
463 **self._extra_prompt_options()
472 **self._extra_prompt_options()
464 )
473 )
474 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
475 self.auto_suggest.connect(self.pt_app)
465
476
466 def _make_style_from_name_or_cls(self, name_or_cls):
477 def _make_style_from_name_or_cls(self, name_or_cls):
467 """
478 """
@@ -649,6 +660,7 b' class TerminalInteractiveShell(InteractiveShell):'
649
660
650 def __init__(self, *args, **kwargs):
661 def __init__(self, *args, **kwargs):
651 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
662 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
663 self.auto_suggest: UnionType[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None] = None
652 self._set_autosuggestions(self.autosuggestions_provider)
664 self._set_autosuggestions(self.autosuggestions_provider)
653 self.init_prompt_toolkit_cli()
665 self.init_prompt_toolkit_cli()
654 self.init_term_title()
666 self.init_term_title()
@@ -34,12 +34,24 b' from prompt_toolkit.key_binding.vi_state import InputMode, ViState'
34 from prompt_toolkit.layout.layout import FocusableElement
34 from prompt_toolkit.layout.layout import FocusableElement
35
35
36 from IPython.utils.decorators import undoc
36 from IPython.utils.decorators import undoc
37 from . import auto_match as match, autosuggestions
37 from . import auto_match as match, auto_suggest
38
38
39
39
40 __all__ = ["create_ipython_shortcuts"]
40 __all__ = ["create_ipython_shortcuts"]
41
41
42
42
43 try:
44 # only added in 3.0.30
45 from prompt_toolkit.filters import has_suggestion
46 except ImportError:
47
48 @undoc
49 @Condition
50 def has_suggestion():
51 buffer = get_app().current_buffer
52 return buffer.suggestion is not None and buffer.suggestion.text != ""
53
54
43 @undoc
55 @undoc
44 @Condition
56 @Condition
45 def cursor_in_leading_ws():
57 def cursor_in_leading_ws():
@@ -324,16 +336,27 b' def create_ipython_shortcuts(shell, for_all_platforms: bool = False):'
324
336
325 # autosuggestions
337 # autosuggestions
326 kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))(
338 kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))(
327 autosuggestions.accept_in_vi_insert_mode
339 auto_suggest.accept_in_vi_insert_mode
328 )
340 )
329 kb.add("c-e", filter=focused_insert_vi & ebivim)(
341 kb.add("c-e", filter=focused_insert_vi & ebivim)(
330 autosuggestions.accept_in_vi_insert_mode
342 auto_suggest.accept_in_vi_insert_mode
343 )
344 kb.add("c-f", filter=focused_insert_vi)(auto_suggest.accept)
345 kb.add("escape", "f", filter=focused_insert_vi & ebivim)(auto_suggest.accept_word)
346 kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
347 auto_suggest.accept_token
348 )
349 from functools import partial
350
351 kb.add("up", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
352 auto_suggest.swap_autosuggestion_up(shell.auto_suggest)
353 )
354 kb.add("down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
355 auto_suggest.swap_autosuggestion_down(shell.auto_suggest)
331 )
356 )
332 kb.add("c-f", filter=focused_insert_vi)(autosuggestions.accept)
357 kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
333 kb.add("escape", "f", filter=focused_insert_vi & ebivim)(
358 auto_suggest.accept_character
334 autosuggestions.accept_word
335 )
359 )
336 kb.add("c-right", filter=has_focus(DEFAULT_BUFFER))(autosuggestions.accept_token)
337
360
338 # Simple Control keybindings
361 # Simple Control keybindings
339 key_cmd_dict = {
362 key_cmd_dict = {
@@ -1,5 +1,5 b''
1 import pytest
1 import pytest
2 from IPython.terminal.shortcuts.autosuggestions import (
2 from IPython.terminal.shortcuts.auto_suggest import (
3 accept_in_vi_insert_mode,
3 accept_in_vi_insert_mode,
4 accept_token,
4 accept_token,
5 )
5 )
@@ -101,6 +101,7 b' class _DummyTerminal:'
101 input_transformer_manager = None
101 input_transformer_manager = None
102 display_completions = None
102 display_completions = None
103 editing_mode = "emacs"
103 editing_mode = "emacs"
104 auto_suggest = None
104
105
105
106
106 def create_identifier(handler: Callable):
107 def create_identifier(handler: Callable):
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now